Поправил csv импорт
This commit is contained in:
@@ -765,31 +765,37 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
|||||||
'errors': список ошибок
|
'errors': список ошибок
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
# Читаем CSV без предопределенных имен колонок
|
||||||
df = pd.read_csv(
|
df = pd.read_csv(
|
||||||
io.StringIO(file_content),
|
io.StringIO(file_content),
|
||||||
sep=";",
|
sep=";",
|
||||||
names=[
|
header=None
|
||||||
"id",
|
|
||||||
"obj",
|
|
||||||
"lat",
|
|
||||||
"lon",
|
|
||||||
"h",
|
|
||||||
"time",
|
|
||||||
"sat",
|
|
||||||
"norad_id",
|
|
||||||
"freq",
|
|
||||||
"f_range",
|
|
||||||
"et",
|
|
||||||
"qaul",
|
|
||||||
"mir_1",
|
|
||||||
"mir_2",
|
|
||||||
"mir_3",
|
|
||||||
"mir_4",
|
|
||||||
"mir_5",
|
|
||||||
"mir_6",
|
|
||||||
"mir_7",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Присваиваем имена первым 12 колонкам
|
||||||
|
base_columns = [
|
||||||
|
"id",
|
||||||
|
"obj",
|
||||||
|
"lat",
|
||||||
|
"lon",
|
||||||
|
"h",
|
||||||
|
"time",
|
||||||
|
"sat",
|
||||||
|
"norad_id",
|
||||||
|
"freq",
|
||||||
|
"f_range",
|
||||||
|
"et",
|
||||||
|
"qual",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Все колонки после "qual" (индекс 11) - это зеркала
|
||||||
|
num_columns = len(df.columns)
|
||||||
|
mirror_columns = [f"mir_{i+1}" for i in range(num_columns - len(base_columns))]
|
||||||
|
|
||||||
|
# Объединяем имена колонок
|
||||||
|
df.columns = base_columns + mirror_columns
|
||||||
|
|
||||||
|
# Преобразуем типы данных
|
||||||
df[["lat", "lon", "freq", "f_range"]] = (
|
df[["lat", "lon", "freq", "f_range"]] = (
|
||||||
df[["lat", "lon", "freq", "f_range"]]
|
df[["lat", "lon", "freq", "f_range"]]
|
||||||
.replace(",", ".", regex=True)
|
.replace(",", ".", regex=True)
|
||||||
@@ -873,7 +879,7 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
|||||||
sources_cache[(source_name, sat_name, source.id)] = source
|
sources_cache[(source_name, sat_name, source.id)] = source
|
||||||
|
|
||||||
# Создаем ObjItem (с Source или без, в зависимости от is_automatic)
|
# Создаем ObjItem (с Source или без, в зависимости от is_automatic)
|
||||||
_create_objitem_from_csv_row(row, source, user_to_use, is_automatic)
|
_create_objitem_from_csv_row(row, source, user_to_use, is_automatic, mirror_columns)
|
||||||
added_count += 1
|
added_count += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -996,7 +1002,7 @@ def _find_tech_analyze_data(name: str, satellite: Satellite):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
|
def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False, mirror_columns=None):
|
||||||
"""
|
"""
|
||||||
Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
|
Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
|
||||||
|
|
||||||
@@ -1008,6 +1014,7 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
|
|||||||
source: объект Source для связи (может быть None если is_automatic=True)
|
source: объект Source для связи (может быть None если is_automatic=True)
|
||||||
user_to_use: пользователь для created_by
|
user_to_use: пользователь для created_by
|
||||||
is_automatic: если True, точка не связывается с Source
|
is_automatic: если True, точка не связывается с Source
|
||||||
|
mirror_columns: список имен колонок с зеркалами (optional)
|
||||||
"""
|
"""
|
||||||
# Определяем поляризацию
|
# Определяем поляризацию
|
||||||
match row["obj"].split(" ")[-1]:
|
match row["obj"].split(" ")[-1]:
|
||||||
@@ -1035,12 +1042,24 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
|
|||||||
|
|
||||||
# Обработка зеркал - теперь это спутники
|
# Обработка зеркал - теперь это спутники
|
||||||
mirror_names = []
|
mirror_names = []
|
||||||
if not pd.isna(row["mir_1"]) and row["mir_1"].strip() != "-":
|
|
||||||
mirror_names.append(row["mir_1"])
|
# Если переданы имена колонок зеркал, используем их
|
||||||
if not pd.isna(row["mir_2"]) and row["mir_2"].strip() != "-":
|
if mirror_columns:
|
||||||
mirror_names.append(row["mir_2"])
|
for mir_col in mirror_columns:
|
||||||
if not pd.isna(row["mir_3"]) and row["mir_3"].strip() != "-":
|
if mir_col in row.index:
|
||||||
mirror_names.append(row["mir_3"])
|
mir_value = row[mir_col]
|
||||||
|
if not pd.isna(mir_value) and str(mir_value).strip() != "-" and str(mir_value).strip() != "":
|
||||||
|
mirror_names.append(str(mir_value).strip())
|
||||||
|
else:
|
||||||
|
# Fallback на старый способ (для обратной совместимости)
|
||||||
|
for i in range(1, 100): # Проверяем до 100 колонок зеркал
|
||||||
|
mir_col = f"mir_{i}"
|
||||||
|
if mir_col in row.index:
|
||||||
|
mir_value = row[mir_col]
|
||||||
|
if not pd.isna(mir_value) and str(mir_value).strip() != "-" and str(mir_value).strip() != "":
|
||||||
|
mirror_names.append(str(mir_value).strip())
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
# Находим спутники-зеркала
|
# Находим спутники-зеркала
|
||||||
mirror_satellites = find_mirror_satellites(mirror_names)
|
mirror_satellites = find_mirror_satellites(mirror_names)
|
||||||
|
|||||||
@@ -223,8 +223,11 @@ def parse_transponders_from_json(filepath: str, user=None):
|
|||||||
|
|
||||||
|
|
||||||
# Third-party imports (additional)
|
# Third-party imports (additional)
|
||||||
|
import logging
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
||||||
"""
|
"""
|
||||||
Парсит транспондеры из XML файла.
|
Парсит транспондеры из XML файла.
|
||||||
@@ -232,9 +235,23 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
Если имя спутника содержит альтернативное имя в скобках, оно извлекается
|
Если имя спутника содержит альтернативное имя в скобках, оно извлекается
|
||||||
и сохраняется в поле alternative_name.
|
и сохраняется в поле alternative_name.
|
||||||
|
|
||||||
|
Процесс импорта:
|
||||||
|
1. Сначала создаются/обновляются все спутники
|
||||||
|
2. Затем для каждого спутника добавляются его транспондеры
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data_in: BytesIO объект с XML данными
|
data_in: BytesIO объект с XML данными
|
||||||
user: пользователь для установки created_by и updated_by (optional)
|
user: пользователь для установки created_by и updated_by (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Статистика импорта с ключами:
|
||||||
|
- satellites_created: количество созданных спутников
|
||||||
|
- satellites_updated: количество обновлённых спутников
|
||||||
|
- satellites_skipped: количество пропущенных спутников (дубликаты)
|
||||||
|
- satellites_ignored: количество игнорированных спутников (X, DONT USE)
|
||||||
|
- transponders_created: количество созданных транспондеров
|
||||||
|
- transponders_existing: количество существующих транспондеров
|
||||||
|
- errors: список ошибок с деталями
|
||||||
"""
|
"""
|
||||||
tree = etree.parse(data_in)
|
tree = etree.parse(data_in)
|
||||||
ns = {
|
ns = {
|
||||||
@@ -243,48 +260,38 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
|
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
|
||||||
}
|
}
|
||||||
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
|
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
|
||||||
for sat in satellites[:]:
|
|
||||||
|
# Статистика импорта
|
||||||
|
stats = {
|
||||||
|
'satellites_created': 0,
|
||||||
|
'satellites_updated': 0,
|
||||||
|
'satellites_skipped': 0,
|
||||||
|
'satellites_ignored': 0,
|
||||||
|
'transponders_created': 0,
|
||||||
|
'transponders_existing': 0,
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Этап 1: Создание/обновление спутников
|
||||||
|
satellite_map = {} # Словарь для связи XML элементов со спутниками в БД
|
||||||
|
|
||||||
|
for sat in satellites:
|
||||||
name_full = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
name_full = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||||
|
|
||||||
|
# Игнорируем служебные записи
|
||||||
if name_full == 'X' or 'DONT USE' in name_full:
|
if name_full == 'X' or 'DONT USE' in name_full:
|
||||||
|
stats['satellites_ignored'] += 1
|
||||||
|
logger.info(f"Игнорирован спутник: {name_full}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Парсим имя спутника и альтернативное имя
|
# Парсим имя спутника и альтернативное имя
|
||||||
main_name, alt_name = parse_satellite_name(name_full)
|
main_name, alt_name = parse_satellite_name(name_full)
|
||||||
|
|
||||||
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
||||||
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
|
|
||||||
intl_code = sat.xpath('.//ns:internationalCode/text()', namespaces=ns)
|
intl_code = sat.xpath('.//ns:internationalCode/text()', namespaces=ns)
|
||||||
sub_sat_point = sat.xpath('.//ns:subSatellitePoint/text()', namespaces=ns)
|
sub_sat_point = sat.xpath('.//ns:subSatellitePoint/text()', namespaces=ns)
|
||||||
zones = {}
|
|
||||||
for zone in beams:
|
|
||||||
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
|
|
||||||
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
|
|
||||||
"name": zone_name,
|
|
||||||
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
|
|
||||||
}
|
|
||||||
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
|
|
||||||
for transponder in transponders:
|
|
||||||
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
|
|
||||||
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
|
|
||||||
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
|
|
||||||
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
|
|
||||||
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
|
|
||||||
tr_data = zones[tr_id]
|
|
||||||
match tr_data['pol']:
|
|
||||||
case 'Horizontal':
|
|
||||||
pol = 'Горизонтальная'
|
|
||||||
case 'Vertical':
|
|
||||||
pol = 'Вертикальная'
|
|
||||||
case 'CircularRight':
|
|
||||||
pol = 'Правая'
|
|
||||||
case 'CircularLeft':
|
|
||||||
pol = 'Левая'
|
|
||||||
case _:
|
|
||||||
pol = '-'
|
|
||||||
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
|
|
||||||
|
|
||||||
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
|
||||||
|
|
||||||
|
try:
|
||||||
# Ищем спутник по имени или альтернативному имени
|
# Ищем спутник по имени или альтернативному имени
|
||||||
sat_obj = find_satellite_by_name(main_name)
|
sat_obj = find_satellite_by_name(main_name)
|
||||||
if not sat_obj:
|
if not sat_obj:
|
||||||
@@ -296,8 +303,10 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
international_code=intl_code[0] if intl_code else "",
|
international_code=intl_code[0] if intl_code else "",
|
||||||
undersat_point=float(sub_sat_point[0]) if sub_sat_point else None
|
undersat_point=float(sub_sat_point[0]) if sub_sat_point else None
|
||||||
)
|
)
|
||||||
|
stats['satellites_created'] += 1
|
||||||
|
logger.info(f"Создан спутник: {main_name} (альт. имя: {alt_name})")
|
||||||
else:
|
else:
|
||||||
# Если найден, обновляем альтернативное имя если не установлено
|
# Если найден, обновляем поля если они не установлены
|
||||||
updated = False
|
updated = False
|
||||||
if alt_name and not sat_obj.alternative_name:
|
if alt_name and not sat_obj.alternative_name:
|
||||||
sat_obj.alternative_name = alt_name
|
sat_obj.alternative_name = alt_name
|
||||||
@@ -313,20 +322,130 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
updated = True
|
updated = True
|
||||||
if updated:
|
if updated:
|
||||||
sat_obj.save()
|
sat_obj.save()
|
||||||
|
stats['satellites_updated'] += 1
|
||||||
|
logger.info(f"Обновлён спутник: {main_name}")
|
||||||
|
|
||||||
trans_obj, created = Transponders.objects.get_or_create(
|
# Сохраняем связь XML элемента со спутником в БД
|
||||||
polarization=pol_obj,
|
satellite_map[sat] = sat_obj
|
||||||
downlink=(downlink_start+downlink_end)/2/1000000,
|
|
||||||
uplink=(uplink_start+uplink_end)/2/1000000,
|
except Satellite.MultipleObjectsReturned:
|
||||||
frequency_range=abs(downlink_end-downlink_start)/1000000,
|
# Найдено несколько спутников - пропускаем
|
||||||
name=tr_name,
|
stats['satellites_skipped'] += 1
|
||||||
defaults={
|
duplicates = Satellite.objects.filter(
|
||||||
"zone_name": tr_data['name'],
|
Q(name__icontains=main_name.lower()) |
|
||||||
"sat_id": sat_obj,
|
Q(alternative_name__icontains=main_name.lower())
|
||||||
}
|
|
||||||
)
|
)
|
||||||
if user:
|
duplicate_names = [f"{s.name} (ID: {s.id})" for s in duplicates]
|
||||||
if created:
|
error_msg = f"Найдено несколько спутников для '{name_full}': {', '.join(duplicate_names)}"
|
||||||
trans_obj.created_by = user
|
stats['errors'].append({
|
||||||
trans_obj.updated_by = user
|
'type': 'duplicate_satellite',
|
||||||
trans_obj.save()
|
'satellite': name_full,
|
||||||
|
'details': duplicate_names
|
||||||
|
})
|
||||||
|
logger.warning(error_msg)
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
# Другие ошибки при обработке спутника
|
||||||
|
stats['satellites_skipped'] += 1
|
||||||
|
error_msg = f"Ошибка при обработке спутника '{name_full}': {str(e)}"
|
||||||
|
stats['errors'].append({
|
||||||
|
'type': 'satellite_error',
|
||||||
|
'satellite': name_full,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Этап 2: Добавление транспондеров для каждого спутника
|
||||||
|
for sat, sat_obj in satellite_map.items():
|
||||||
|
sat_name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
|
||||||
|
zones = {}
|
||||||
|
for zone in beams:
|
||||||
|
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
|
||||||
|
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
|
||||||
|
"name": zone_name,
|
||||||
|
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
|
||||||
|
for transponder in transponders:
|
||||||
|
try:
|
||||||
|
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
|
||||||
|
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
|
||||||
|
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
|
||||||
|
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
|
||||||
|
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
|
||||||
|
tr_data = zones[tr_id]
|
||||||
|
|
||||||
|
match tr_data['pol']:
|
||||||
|
case 'Horizontal':
|
||||||
|
pol = 'Горизонтальная'
|
||||||
|
case 'Vertical':
|
||||||
|
pol = 'Вертикальная'
|
||||||
|
case 'CircularRight':
|
||||||
|
pol = 'Правая'
|
||||||
|
case 'CircularLeft':
|
||||||
|
pol = 'Левая'
|
||||||
|
case _:
|
||||||
|
pol = '-'
|
||||||
|
|
||||||
|
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||||
|
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
||||||
|
|
||||||
|
trans_obj, created = Transponders.objects.get_or_create(
|
||||||
|
polarization=pol_obj,
|
||||||
|
downlink=(downlink_start+downlink_end)/2/1000000,
|
||||||
|
uplink=(uplink_start+uplink_end)/2/1000000,
|
||||||
|
frequency_range=abs(downlink_end-downlink_start)/1000000,
|
||||||
|
name=tr_name,
|
||||||
|
defaults={
|
||||||
|
"zone_name": tr_data['name'],
|
||||||
|
"sat_id": sat_obj,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
stats['transponders_created'] += 1
|
||||||
|
else:
|
||||||
|
stats['transponders_existing'] += 1
|
||||||
|
|
||||||
|
if user:
|
||||||
|
if created:
|
||||||
|
trans_obj.created_by = user
|
||||||
|
trans_obj.updated_by = user
|
||||||
|
trans_obj.save()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Ошибка при обработке транспондера спутника '{sat_name}': {str(e)}"
|
||||||
|
stats['errors'].append({
|
||||||
|
'type': 'transponder_error',
|
||||||
|
'satellite': sat_name,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Ошибка при обработке транспондеров спутника '{sat_name}': {str(e)}"
|
||||||
|
stats['errors'].append({
|
||||||
|
'type': 'transponders_processing_error',
|
||||||
|
'satellite': sat_name,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Итоговая статистика в лог
|
||||||
|
logger.info(
|
||||||
|
f"Импорт завершён. Спутники: создано {stats['satellites_created']}, "
|
||||||
|
f"обновлено {stats['satellites_updated']}, пропущено {stats['satellites_skipped']}, "
|
||||||
|
f"игнорировано {stats['satellites_ignored']}. "
|
||||||
|
f"Транспондеры: создано {stats['transponders_created']}, "
|
||||||
|
f"существующих {stats['transponders_existing']}. "
|
||||||
|
f"Ошибок: {len(stats['errors'])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return stats
|
||||||
Reference in New Issue
Block a user