Добавил формы создания и пофиксил баг с пользователями

This commit is contained in:
2025-11-24 23:47:00 +03:00
parent 1c18ae96f7
commit 7879c3d9b5
19 changed files with 448 additions and 183 deletions

View File

@@ -271,34 +271,40 @@ def _find_or_create_source_by_name_and_distance(
return source
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None, is_automatic=False):
"""
Импортирует данные из DataFrame с группировкой по имени источника и расстоянию.
Алгоритм:
1. Для каждой строки DataFrame:
a. Извлечь имя источника (из колонки "Объект наблюдения")
b. Найти подходящий Source:
- Ищет все Source с таким же именем и спутником
- Проверяет расстояние до каждого Source
- Если найден Source в радиусе ≤56 км - использует его
- Иначе создает новый Source
c. Обновить coords_average инкрементально
d. Создать ObjItem и связать с Source
b. Если is_automatic=False:
- Найти подходящий Source:
* Ищет все Source с таким же именем и спутником
* Проверяет расстояние до каждого Source
* Если найден Source в радиусе ≤56 км - использует его
* Иначе создает новый Source
- Обновить coords_average инкрементально
- Создать ObjItem и связать с Source
c. Если is_automatic=True:
- Создать ObjItem без связи с Source (source=None)
- Точка просто хранится в базе
Важные правила:
- Источники разных спутников НЕ объединяются
- Может быть несколько Source с одинаковым именем, но разделенных географически
- Точка добавляется к Source только если расстояние ≤56 км
- Координаты усредняются инкрементально для каждого источника
- Если точка уже существует (по координатам и времени ГЛ), она не добавляется повторно
Args:
df: DataFrame с данными
sat: объект Satellite
current_user: текущий пользователь (optional)
is_automatic: если True, точки не добавляются к Source (optional, default=False)
Returns:
int: количество созданных Source
int: количество созданных Source (или 0 если is_automatic=True)
"""
try:
df.rename(columns={"Модуляция ": "Модуляция"}, inplace=True)
@@ -311,6 +317,7 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
new_sources_count = 0
added_count = 0
skipped_count = 0
# Словарь для кэширования Source в рамках текущего импорта
# Ключ: (имя источника, id Source), Значение: объект Source
@@ -325,42 +332,59 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
# Извлекаем имя источника
source_name = row["Объект наблюдения"]
found_in_cache = False
for cache_key, cached_source in sources_cache.items():
cached_name, cached_id = cache_key
# Проверяем имя
if cached_name != source_name:
continue
# Проверяем расстояние
if cached_source.coords_average:
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
_, distance = calculate_mean_coords(source_coord, coord_tuple)
# Извлекаем время для проверки дубликатов
date = row["Дата"].date()
time_ = row["Время"]
if isinstance(time_, str):
time_ = time_.strip()
time_ = time(0, 0, 0)
timestamp = datetime.combine(date, time_)
# Проверяем дубликаты по координатам и времени
if _is_duplicate_by_coords_and_time(coord_tuple, timestamp):
skipped_count += 1
continue
source = None
# Если is_automatic=False, работаем с Source
if not is_automatic:
found_in_cache = False
for cache_key, cached_source in sources_cache.items():
cached_name, cached_id = cache_key
if distance <= RANGE_DISTANCE:
# Нашли подходящий Source в кэше
cached_source.update_coords_average(coord_tuple)
cached_source.save()
source = cached_source
found_in_cache = True
break
if not found_in_cache:
# Ищем в БД или создаем новый Source
source = _find_or_create_source_by_name_and_distance(
source_name, sat, coord_tuple, user_to_use
)
# Проверяем имя
if cached_name != source_name:
continue
# Проверяем расстояние
if cached_source.coords_average:
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
_, distance = calculate_mean_coords(source_coord, coord_tuple)
if distance <= RANGE_DISTANCE:
# Нашли подходящий Source в кэше
cached_source.update_coords_average(coord_tuple)
cached_source.save()
source = cached_source
found_in_cache = True
break
# Проверяем, был ли создан новый Source
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
new_sources_count += 1
# Добавляем в кэш
sources_cache[(source_name, source.id)] = source
if not found_in_cache:
# Ищем в БД или создаем новый Source
source = _find_or_create_source_by_name_and_distance(
source_name, sat, coord_tuple, user_to_use
)
# Проверяем, был ли создан новый Source
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
new_sources_count += 1
# Добавляем в кэш
sources_cache[(source_name, source.id)] = source
# Создаем ObjItem и связываем с Source
_create_objitem_from_row(row, sat, source, user_to_use, consts)
# Создаем ObjItem (с Source или без, в зависимости от is_automatic)
_create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic)
added_count += 1
except Exception as e:
@@ -368,21 +392,22 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
continue
print(f"Импорт завершен: создано {new_sources_count} новых источников, "
f"добавлено {added_count} точек")
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
return new_sources_count
def _create_objitem_from_row(row, sat, source, user_to_use, consts):
def _create_objitem_from_row(row, sat, source, user_to_use, consts, is_automatic=False):
"""
Вспомогательная функция для создания ObjItem из строки DataFrame.
Args:
row: строка DataFrame
sat: объект Satellite
source: объект Source для связи
source: объект Source для связи (может быть None если is_automatic=True)
user_to_use: пользователь для created_by
consts: константы из get_all_constants()
is_automatic: если True, точка не связывается с Source
"""
# Извлекаем координату
geo_point = Point(coords_transform(row["Координаты"]), srid=4326)
@@ -475,12 +500,13 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
# Находим подходящий источник LyngSat (точность 0.1 МГц)
lyngsat_source = find_matching_lyngsat(sat, freq, polarization_obj, tolerance_mhz=0.1)
# Создаем новый ObjItem и связываем с Source, Transponder и LyngSat
# Создаем новый ObjItem и связываем с Source (если не автоматическая), Transponder и LyngSat
obj_item = ObjItem.objects.create(
name=source_name,
source=source,
source=source if not is_automatic else None,
transponder=transponder,
lyngsat_source=lyngsat_source,
is_automatic=is_automatic,
created_by=user_to_use
)
@@ -500,9 +526,10 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
geo.objitem = obj_item
geo.save()
# Обновляем дату подтверждения источника
source.update_confirm_at()
source.save()
# Обновляем дату подтверждения источника (только если не автоматическая)
if source and not is_automatic:
source.update_confirm_at()
source.save()
def add_satellite_list():
@@ -572,34 +599,40 @@ def get_point_from_json(filepath: str):
)
def get_points_from_csv(file_content, current_user=None):
def get_points_from_csv(file_content, current_user=None, is_automatic=False):
"""
Импортирует данные из CSV с группировкой по имени источника и расстоянию.
Алгоритм:
1. Для каждой строки CSV:
a. Извлечь имя источника (из колонки "obj") и спутник
b. Проверить дубликаты (координаты + частота)
c. Найти подходящий Source:
- Ищет все Source с таким же именем и спутником
- Проверяет расстояние до каждого Source
- Если найден Source в радиусе ≤56 км - использует его
- Иначе создает новый Source
d. Обновить coords_average инкрементально
e. Создать ObjItem и связать с Source
b. Проверить дубликаты (координаты + время ГЛ)
c. Если is_automatic=False:
- Найти подходящий Source:
* Ищет все Source с таким же именем и спутником
* Проверяет расстояние до каждого Source
* Если найден Source в радиусе ≤56 км - использует его
* Иначе создает новый Source
- Обновить coords_average инкрементально
- Создать ObjItem и связать с Source
d. Если is_automatic=True:
- Создать ObjItem без связи с Source (source=None)
- Точка просто хранится в базе
Важные правила:
- Источники разных спутников НЕ объединяются
- Может быть несколько Source с одинаковым именем, но разделенных географически
- Точка добавляется к Source только если расстояние ≤56 км
- Координаты усредняются инкрементально для каждого источника
- Если точка уже существует (по координатам и времени ГЛ), она не добавляется повторно
Args:
file_content: содержимое CSV файла
current_user: текущий пользователь (optional)
is_automatic: если True, точки не добавляются к Source (optional, default=False)
Returns:
int: количество созданных Source
int: количество созданных Source (или 0 если is_automatic=True)
"""
df = pd.read_csv(
io.StringIO(file_content),
@@ -647,8 +680,11 @@ def get_points_from_csv(file_content, current_user=None):
source_name = row["obj"]
sat_name = row["sat"]
# Проверяем дубликаты
if _is_duplicate_objitem(coord_tuple, row["freq"], row["f_range"]):
# Извлекаем время для проверки дубликатов
timestamp = row["time"]
# Проверяем дубликаты по координатам и времени
if _is_duplicate_by_coords_and_time(coord_tuple, timestamp):
skipped_count += 1
continue
@@ -657,43 +693,47 @@ def get_points_from_csv(file_content, current_user=None):
name=sat_name, defaults={"norad": row["norad_id"]}
)
# Проверяем кэш: ищем подходящий Source среди закэшированных
found_in_cache = False
for cache_key, cached_source in sources_cache.items():
cached_name, cached_sat, cached_id = cache_key
# Проверяем имя и спутник
if cached_name != source_name or cached_sat != sat_name:
continue
# Проверяем расстояние
if cached_source.coords_average:
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
_, distance = calculate_mean_coords(source_coord, coord_tuple)
source = None
# Если is_automatic=False, работаем с Source
if not is_automatic:
# Проверяем кэш: ищем подходящий Source среди закэшированных
found_in_cache = False
for cache_key, cached_source in sources_cache.items():
cached_name, cached_sat, cached_id = cache_key
if distance <= RANGE_DISTANCE:
# Нашли подходящий Source в кэше
cached_source.update_coords_average(coord_tuple)
cached_source.save()
source = cached_source
found_in_cache = True
break
if not found_in_cache:
# Ищем в БД или создаем новый Source
source = _find_or_create_source_by_name_and_distance(
source_name, sat_obj, coord_tuple, user_to_use
)
# Проверяем имя и спутник
if cached_name != source_name or cached_sat != sat_name:
continue
# Проверяем расстояние
if cached_source.coords_average:
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
_, distance = calculate_mean_coords(source_coord, coord_tuple)
if distance <= RANGE_DISTANCE:
# Нашли подходящий Source в кэше
cached_source.update_coords_average(coord_tuple)
cached_source.save()
source = cached_source
found_in_cache = True
break
# Проверяем, был ли создан новый Source
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
new_sources_count += 1
# Добавляем в кэш
sources_cache[(source_name, sat_name, source.id)] = source
if not found_in_cache:
# Ищем в БД или создаем новый Source
source = _find_or_create_source_by_name_and_distance(
source_name, sat_obj, coord_tuple, user_to_use
)
# Проверяем, был ли создан новый Source
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
new_sources_count += 1
# Добавляем в кэш
sources_cache[(source_name, sat_name, source.id)] = source
# Создаем ObjItem и связываем с Source
_create_objitem_from_csv_row(row, source, user_to_use)
# Создаем ObjItem (с Source или без, в зависимости от is_automatic)
_create_objitem_from_csv_row(row, source, user_to_use, is_automatic)
added_count += 1
except Exception as e:
@@ -706,6 +746,39 @@ def get_points_from_csv(file_content, current_user=None):
return new_sources_count
def _is_duplicate_by_coords_and_time(coord_tuple, timestamp, tolerance_km=0.1):
"""
Проверяет, существует ли уже ObjItem с такими же координатами и временем ГЛ.
Args:
coord_tuple: кортеж (lon, lat) координат
timestamp: время ГЛ (datetime)
tolerance_km: допуск для сравнения координат в километрах (default=0.1)
Returns:
bool: True если дубликат найден, False иначе
"""
# Ищем Geo с таким же timestamp и близкими координатами
existing_geo = Geo.objects.filter(
timestamp=timestamp,
coords__isnull=False
)
for geo in existing_geo:
if not geo.coords:
continue
# Проверяем расстояние между координатами
geo_coord = (geo.coords.x, geo.coords.y)
_, distance = calculate_mean_coords(coord_tuple, geo_coord)
if distance <= tolerance_km:
# Найден дубликат
return True
return False
def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
"""
Проверяет, существует ли уже ObjItem с такими же координатами и частотой.
@@ -744,14 +817,15 @@ def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
return False
def _create_objitem_from_csv_row(row, source, user_to_use):
def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
"""
Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
Args:
row: строка DataFrame
source: объект Source для связи
source: объект Source для связи (может быть None если is_automatic=True)
user_to_use: пользователь для created_by
is_automatic: если True, точка не связывается с Source
"""
# Определяем поляризацию
match row["obj"].split(" ")[-1]:
@@ -817,12 +891,13 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
# Находим подходящий источник LyngSat (точность 0.1 МГц)
lyngsat_source = find_matching_lyngsat(sat_obj, row["freq"], pol_obj, tolerance_mhz=0.1)
# Создаем новый ObjItem и связываем с Source, Transponder и LyngSat
# Создаем новый ObjItem и связываем с Source (если не автоматическая), Transponder и LyngSat
obj_item = ObjItem.objects.create(
name=row["obj"],
source=source,
source=source if not is_automatic else None,
transponder=transponder,
lyngsat_source=lyngsat_source,
is_automatic=is_automatic,
created_by=user_to_use
)
@@ -839,9 +914,10 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
geo_obj.objitem = obj_item
geo_obj.save()
# Обновляем дату подтверждения источника
source.update_confirm_at()
source.save()
# Обновляем дату подтверждения источника (только если не автоматическая)
if source and not is_automatic:
source.update_confirm_at()
source.save()
def get_vch_load_from_html(file, sat: Satellite) -> None: