diff --git a/dbapp/fix_objitems_without_source.py b/dbapp/fix_objitems_without_source.py
new file mode 100644
index 0000000..c1a6aea
--- /dev/null
+++ b/dbapp/fix_objitems_without_source.py
@@ -0,0 +1,99 @@
+"""
+Скрипт для исправления ObjItems без связи с Source.
+
+Для каждого ObjItem без source:
+1. Получить координаты из geo_obj
+2. Найти ближайший Source (по coords_average)
+3. Если расстояние <= 0.5 градуса, связать ObjItem с этим Source
+4. Иначе создать новый Source с coords_average = координаты geo_obj
+"""
+
+import os
+import django
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dbapp.settings")
+django.setup()
+
+from mainapp.models import ObjItem, Source, CustomUser
+from django.contrib.gis.geos import Point
+from django.contrib.gis.measure import D
+from django.contrib.gis.db.models.functions import Distance
+
+
+def calculate_distance_degrees(coord1, coord2):
+ """Вычисляет расстояние между двумя координатами в градусах."""
+ import math
+
+ lon1, lat1 = coord1
+ lon2, lat2 = coord2
+
+ return math.sqrt((lon2 - lon1) ** 2 + (lat2 - lat1) ** 2)
+
+
+def fix_objitems_without_source():
+ """Исправляет ObjItems без связи с Source."""
+
+ # Получаем пользователя по умолчанию
+ default_user = CustomUser.objects.get(id=1)
+
+ # Получаем все ObjItems без source
+ objitems_without_source = ObjItem.objects.filter(source__isnull=True)
+ total_count = objitems_without_source.count()
+
+ print(f"Найдено {total_count} ObjItems без source")
+
+ if total_count == 0:
+ print("Нечего исправлять!")
+ return
+
+ fixed_count = 0
+ new_sources_count = 0
+
+ for objitem in objitems_without_source:
+ # Проверяем, есть ли geo_obj
+ if not hasattr(objitem, 'geo_obj') or not objitem.geo_obj or not objitem.geo_obj.coords:
+ print(f"ObjItem {objitem.id} не имеет geo_obj или координат, пропускаем")
+ continue
+
+ geo_coords = objitem.geo_obj.coords
+ coord_tuple = (geo_coords.x, geo_coords.y)
+
+ # Ищем ближайший Source
+ sources_with_coords = Source.objects.filter(coords_average__isnull=False)
+
+ closest_source = None
+ min_distance = float('inf')
+
+ for source in sources_with_coords:
+ source_coord = (source.coords_average.x, source.coords_average.y)
+ distance = calculate_distance_degrees(coord_tuple, source_coord)
+
+ if distance < min_distance:
+ min_distance = distance
+ closest_source = source
+
+ # Если нашли близкий Source (расстояние <= 0.5 градуса)
+ if closest_source and min_distance <= 0.5:
+ objitem.source = closest_source
+ objitem.save()
+ print(f"ObjItem {objitem.id} связан с Source {closest_source.id} (расстояние: {min_distance:.4f}°)")
+ fixed_count += 1
+ else:
+ # Создаем новый Source
+ new_source = Source.objects.create(
+ coords_average=Point(coord_tuple, srid=4326),
+ created_by=default_user
+ )
+ objitem.source = new_source
+ objitem.save()
+ print(f"ObjItem {objitem.id} связан с новым Source {new_source.id}")
+ fixed_count += 1
+ new_sources_count += 1
+
+ print(f"\nГотово!")
+ print(f"Исправлено ObjItems: {fixed_count}")
+ print(f"Создано новых Source: {new_sources_count}")
+
+
+if __name__ == "__main__":
+ fix_objitems_without_source()
diff --git a/dbapp/mainapp/migrations/0003_source_coords_average_alter_objitem_source_and_more.py b/dbapp/mainapp/migrations/0003_source_coords_average_alter_objitem_source_and_more.py
new file mode 100644
index 0000000..a66aa0a
--- /dev/null
+++ b/dbapp/mainapp/migrations/0003_source_coords_average_alter_objitem_source_and_more.py
@@ -0,0 +1,31 @@
+# Generated by Django 5.2.7 on 2025-11-12 19:41
+
+import django.contrib.gis.db.models.fields
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mainapp', '0002_initial'),
+ ('mapsapp', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='source',
+ name='coords_average',
+ field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Усреднённые координаты, полученные от в ходе геолокации (WGS84)', null=True, srid=4326, verbose_name='Координаты ГЛ'),
+ ),
+ migrations.AlterField(
+ model_name='objitem',
+ name='source',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source_objitems', to='mainapp.source', verbose_name='ИРИ'),
+ ),
+ migrations.AlterField(
+ model_name='objitem',
+ name='transponder',
+ field=models.ForeignKey(blank=True, help_text='Транспондер, с помощью которого была получена точка', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transponder_objitems', to='mapsapp.transponders', verbose_name='Транспондер'),
+ ),
+ ]
diff --git a/dbapp/mainapp/templates/mainapp/source_list.html b/dbapp/mainapp/templates/mainapp/source_list.html
new file mode 100644
index 0000000..bf04666
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/source_list.html
@@ -0,0 +1,453 @@
+{% extends 'mainapp/base.html' %}
+
+{% block title %}Список источников{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
Список источников
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Связанные объекты (0):
+
+
+
+
+ | Имя |
+ Спутник |
+ Частота, МГц |
+ Полоса, МГц |
+ Поляризация |
+ Сим. скорость, БОД |
+ Модуляция |
+ ОСШ |
+ Время ГЛ |
+ Местоположение |
+ Координаты ГЛ |
+
+
+
+
+
+
+
+
+
+ Нет связанных объектов
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_js %}
+
+{% endblock %}
diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py
index 0808daf..0e1dbee 100644
--- a/dbapp/mainapp/urls.py
+++ b/dbapp/mainapp/urls.py
@@ -6,7 +6,7 @@ from . import views
app_name = 'mainapp'
urlpatterns = [
- path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
+ path('', views.SourceListView.as_view(), name='home'), # Source list page
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
@@ -23,6 +23,7 @@ urlpatterns = [
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
path('api/lyngsat//', views.LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
path('api/sigma-parameter//', views.SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
+ path('api/source//objitems/', views.SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
path('object//edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py
index 2942aef..6737264 100644
--- a/dbapp/mainapp/utils.py
+++ b/dbapp/mainapp/utils.py
@@ -24,6 +24,7 @@ from .models import (
Polarization,
Satellite,
SigmaParameter,
+ Source,
Standard,
)
@@ -78,139 +79,233 @@ def remove_str(s: str):
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
+ """
+ Импортирует данные из DataFrame с группировкой близких координат.
+
+ Алгоритм:
+ 1. Извлечь все координаты и данные строк из DataFrame
+ 2. Создать список необработанных записей (координата + данные строки)
+ 3. Пока список не пуст:
+ a. Взять первую запись из списка
+ b. Создать новый Source с coords_average = эта координата
+ c. Создать ObjItem для этой записи и связать с Source
+ d. Удалить запись из списка
+ e. Для каждой оставшейся записи в списке:
+ - Вычислить расстояние от её координаты до coords_average
+ - Если расстояние <= 0.5 градуса:
+ * Вычислить новое среднее ИНКРЕМЕНТАЛЬНО:
+ new_avg = (coords_average + current_coord) / 2
+ * Обновить coords_average в Source
+ * Создать ObjItem для этой записи и связать с Source
+ * Удалить запись из списка
+ - Иначе: пропустить и проверить следующую запись
+ 4. Сохранить все изменения в БД
+
+ Важно: Среднее вычисляется инкрементально - каждая новая точка
+ усредняется с текущим средним, а не со всеми точками кластера.
+
+ Args:
+ df: DataFrame с данными
+ sat: объект Satellite
+ current_user: текущий пользователь (optional)
+
+ Returns:
+ int: количество созданных Source
+ """
try:
df.rename(columns={"Модуляция ": "Модуляция"}, inplace=True)
except Exception as e:
print(e)
+
consts = get_all_constants()
df.fillna(-1, inplace=True)
- for stroka in df.iterrows():
- geo_point = Point(coords_transform(stroka[1]["Координаты"]), srid=4326)
- valid_point = None
- kupsat_point = None
+
+ # Шаг 1: Извлечь все координаты и данные строк из DataFrame
+ unprocessed_records = []
+
+ for idx, row in df.iterrows():
try:
- if (
- stroka[1]["Координаты объекта"] != -1
- and stroka[1]["Координаты Кубсата"] != "+"
- ):
- if (
- "ИРИ" not in stroka[1]["Координаты объекта"]
- and "БЛА" not in stroka[1]["Координаты объекта"]
- ):
- valid_point = list(
- map(
- float,
- stroka[1]["Координаты объекта"]
- .replace(",", ".")
- .split(". "),
- )
- )
- valid_point = Point(valid_point[1], valid_point[0], srid=4326)
- if (
- stroka[1]["Координаты Кубсата"] != -1
- and stroka[1]["Координаты Кубсата"] != "+"
- ):
- kupsat_point = list(
- map(
- float,
- stroka[1]["Координаты Кубсата"].replace(",", ".").split(". "),
- )
+ # Извлекаем координату
+ coord_tuple = coords_transform(row["Координаты"])
+
+ # Сохраняем запись с координатой и данными строки
+ unprocessed_records.append({"coord": coord_tuple, "row": row, "index": idx})
+ except Exception as e:
+ print(f"Ошибка при обработке строки {idx}: {e}")
+ continue
+
+ user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
+ source_count = 0
+
+ # Шаг 3: Цикл обработки пока список не пуст
+ while unprocessed_records:
+ # Шаг 3a: Взять первую запись из списка
+ first_record = unprocessed_records.pop(0)
+ first_coord = first_record["coord"]
+
+ # Шаг 3b: Создать новый Source с coords_average = эта координата
+ source = Source.objects.create(
+ coords_average=Point(first_coord, srid=4326), created_by=user_to_use
+ )
+ source_count += 1
+
+ # Шаг 3c: Создать ObjItem для этой записи и связать с Source
+ _create_objitem_from_row(first_record["row"], sat, source, user_to_use, consts)
+
+ # Шаг 3e: Для каждой оставшейся записи в списке
+ records_to_remove = []
+
+ for i, record in enumerate(unprocessed_records):
+ current_coord = record["coord"]
+
+ # Вычислить расстояние от координаты до coords_average
+ current_avg = (source.coords_average.x, source.coords_average.y)
+ distance = calculate_distance_degrees(current_avg, current_coord)
+
+ # Если расстояние <= 0.5 градуса
+ if distance <= 0.5:
+ # Вычислить новое среднее ИНКРЕМЕНТАЛЬНО
+ new_avg = calculate_average_coords_incremental(
+ current_avg, current_coord
)
- kupsat_point = Point(kupsat_point[1], kupsat_point[0], srid=4326)
- except KeyError:
- print("В таблице нет столбцов с координатами кубсата")
- try:
- polarization_obj, _ = Polarization.objects.get_or_create(
- name=stroka[1]["Поляризация"].strip()
- )
- except KeyError:
- polarization_obj, _ = Polarization.objects.get_or_create(name="-")
- freq = remove_str(stroka[1]["Частота, МГц"])
- freq_line = remove_str(stroka[1]["Полоса, МГц"])
- v = remove_str(stroka[1]["Символьная скорость, БОД"])
- try:
- mod_obj, _ = Modulation.objects.get_or_create(
- name=stroka[1]["Модуляция"].strip()
- )
- except AttributeError:
- mod_obj, _ = Modulation.objects.get_or_create(name="-")
- snr = remove_str(stroka[1]["ОСШ"])
- date = stroka[1]["Дата"].date()
- time_ = stroka[1]["Время"]
- if isinstance(time_, str):
- time_ = time_.strip()
- time_ = time(0, 0, 0)
- timestamp = datetime.combine(date, time_)
- current_mirrors = []
- mirror_1 = stroka[1]["Зеркало 1"].strip().split("\n")
- mirror_2 = stroka[1]["Зеркало 2"].strip().split("\n")
- if len(mirror_1) > 1:
- for mir in mirror_1:
- mir_obj, _ = Mirror.objects.get_or_create(name=mir.strip())
- current_mirrors.append(mir.strip())
- elif mirror_1[0] not in consts[3]:
- mir_obj, _ = Mirror.objects.get_or_create(name=mirror_1[0].strip())
- current_mirrors.append(mirror_1[0].strip())
- if len(mirror_2) > 1:
- for mir in mirror_2:
- mir_obj, _ = Mirror.objects.get_or_create(name=mir.strip())
- current_mirrors.append(mir.strip())
- elif mirror_2[0] not in consts[3]:
- mir_obj, _ = Mirror.objects.get_or_create(name=mirror_2[0].strip())
- current_mirrors.append(mirror_2[0].strip())
- location = stroka[1]["Местоопределение"].strip()
- comment = stroka[1]["Комментарий"]
- source = stroka[1]["Объект наблюдения"]
- user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
- geo, _ = Geo.objects.get_or_create(
- timestamp=timestamp,
- coords=geo_point,
- defaults={
- "coords_kupsat": kupsat_point,
- "coords_valid": valid_point,
- "location": location,
- "comment": comment,
- "is_average": (comment != -1.0),
- },
- )
- geo.save()
- geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
+ # Обновить coords_average в Source
+ source.coords_average = Point(new_avg, srid=4326)
+ source.save()
- # Check if ObjItem with same geo already exists
- existing_obj_item = ObjItem.objects.filter(geo_obj=geo).first()
- if existing_obj_item:
- # Check if parameter with same values exists for this object
- if (
- hasattr(existing_obj_item, 'parameter_obj') and
- existing_obj_item.parameter_obj and
- existing_obj_item.parameter_obj.id_satellite == sat and
- existing_obj_item.parameter_obj.polarization == polarization_obj and
- existing_obj_item.parameter_obj.frequency == freq and
- existing_obj_item.parameter_obj.freq_range == freq_line and
- existing_obj_item.parameter_obj.bod_velocity == v and
- existing_obj_item.parameter_obj.modulation == mod_obj and
- existing_obj_item.parameter_obj.snr == snr
- ):
- # Skip creating duplicate
- continue
-
- # Create new ObjItem and Parameter
- obj_item = ObjItem.objects.create(name=source, created_by=user_to_use)
-
- vch_load_obj = Parameter.objects.create(
- id_satellite=sat,
- polarization=polarization_obj,
- frequency=freq,
- freq_range=freq_line,
- bod_velocity=v,
- modulation=mod_obj,
- snr=snr,
- objitem=obj_item
+ # Создать ObjItem для этой записи и связать с Source
+ _create_objitem_from_row(
+ record["row"], sat, source, user_to_use, consts
+ )
+
+ # Пометить запись для удаления
+ records_to_remove.append(i)
+
+ # Удалить обработанные записи из списка (в обратном порядке, чтобы не сбить индексы)
+ for i in reversed(records_to_remove):
+ unprocessed_records.pop(i)
+
+ return source_count
+
+
+def _create_objitem_from_row(row, sat, source, user_to_use, consts):
+ """
+ Вспомогательная функция для создания ObjItem из строки DataFrame.
+
+ Args:
+ row: строка DataFrame
+ sat: объект Satellite
+ source: объект Source для связи
+ user_to_use: пользователь для created_by
+ consts: константы из get_all_constants()
+ """
+ # Извлекаем координату
+ geo_point = Point(coords_transform(row["Координаты"]), srid=4326)
+
+ # Обработка поляризации
+ try:
+ polarization_obj, _ = Polarization.objects.get_or_create(
+ name=row["Поляризация"].strip()
)
-
- geo.objitem = obj_item
- geo.save()
+ except KeyError:
+ polarization_obj, _ = Polarization.objects.get_or_create(name="-")
+
+ # Обработка ВЧ параметров
+ freq = remove_str(row["Частота, МГц"])
+ freq_line = remove_str(row["Полоса, МГц"])
+ v = remove_str(row["Символьная скорость, БОД"])
+
+ try:
+ mod_obj, _ = Modulation.objects.get_or_create(name=row["Модуляция"].strip())
+ except AttributeError:
+ mod_obj, _ = Modulation.objects.get_or_create(name="-")
+
+ snr = remove_str(row["ОСШ"])
+
+ # Обработка времени
+ date = row["Дата"].date()
+ time_ = row["Время"]
+ if isinstance(time_, str):
+ time_ = time_.strip()
+ time_ = time(0, 0, 0)
+ timestamp = datetime.combine(date, time_)
+
+ # Обработка зеркал
+ current_mirrors = []
+ mirror_1 = row["Зеркало 1"].strip().split("\n")
+ mirror_2 = row["Зеркало 2"].strip().split("\n")
+
+ if len(mirror_1) > 1:
+ for mir in mirror_1:
+ Mirror.objects.get_or_create(name=mir.strip())
+ current_mirrors.append(mir.strip())
+ elif mirror_1[0] not in consts[3]:
+ Mirror.objects.get_or_create(name=mirror_1[0].strip())
+ current_mirrors.append(mirror_1[0].strip())
+
+ if len(mirror_2) > 1:
+ for mir in mirror_2:
+ Mirror.objects.get_or_create(name=mir.strip())
+ current_mirrors.append(mir.strip())
+ elif mirror_2[0] not in consts[3]:
+ Mirror.objects.get_or_create(name=mirror_2[0].strip())
+ current_mirrors.append(mirror_2[0].strip())
+
+ location = row["Местоопределение"].strip()
+ comment = row["Комментарий"]
+ source_name = row["Объект наблюдения"]
+
+ # Создаем Geo объект (БЕЗ coords_kupsat и coords_valid)
+ geo, _ = Geo.objects.get_or_create(
+ timestamp=timestamp,
+ coords=geo_point,
+ defaults={
+ "location": location,
+ "comment": comment,
+ "is_average": (comment != -1.0),
+ },
+ )
+ geo.save()
+ geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
+
+ # Проверяем, существует ли уже ObjItem с таким же geo
+ existing_obj_item = ObjItem.objects.filter(geo_obj=geo).first()
+ if existing_obj_item:
+ # Проверяем, существует ли parameter с такими же значениями
+ if (
+ hasattr(existing_obj_item, "parameter_obj")
+ and existing_obj_item.parameter_obj
+ and existing_obj_item.parameter_obj.id_satellite == sat
+ and existing_obj_item.parameter_obj.polarization == polarization_obj
+ and existing_obj_item.parameter_obj.frequency == freq
+ and existing_obj_item.parameter_obj.freq_range == freq_line
+ and existing_obj_item.parameter_obj.bod_velocity == v
+ and existing_obj_item.parameter_obj.modulation == mod_obj
+ and existing_obj_item.parameter_obj.snr == snr
+ ):
+ # Пропускаем создание дубликата
+ return
+
+ # Создаем новый ObjItem и связываем с Source
+ obj_item = ObjItem.objects.create(
+ name=source_name, source=source, created_by=user_to_use
+ )
+
+ # Создаем Parameter
+ Parameter.objects.create(
+ id_satellite=sat,
+ polarization=polarization_obj,
+ frequency=freq,
+ freq_range=freq_line,
+ bod_velocity=v,
+ modulation=mod_obj,
+ snr=snr,
+ objitem=obj_item,
+ )
+
+ # Связываем geo с objitem
+ geo.objitem = obj_item
+ geo.save()
def add_satellite_list():
@@ -281,6 +376,33 @@ def get_point_from_json(filepath: str):
def get_points_from_csv(file_content, current_user=None):
+ """
+ Импортирует данные из CSV с группировкой близких координат.
+
+ Улучшенный алгоритм с учетом существующих Source:
+ 1. Извлечь все координаты и данные строк из DataFrame
+ 2. Создать список необработанных записей (координата + данные строки)
+ 3. Получить все существующие Source из БД
+ 4. Для каждой записи:
+ a. Проверить, существует ли дубликат (координаты + частота)
+ b. Если дубликат найден, пропустить запись
+ c. Найти ближайший существующий Source (расстояние <= 0.5 градуса)
+ d. Если найден:
+ - Обновить coords_average этого Source (инкрементально)
+ - Создать ObjItem и связать с этим Source
+ e. Если не найден:
+ - Создать новый Source
+ - Создать ObjItem и связать с новым Source
+ - Добавить новый Source в список существующих
+ 5. Сохранить все изменения в БД
+
+ Args:
+ file_content: содержимое CSV файла
+ current_user: текущий пользователь (optional)
+
+ Returns:
+ int: количество созданных Source
+ """
df = pd.read_csv(
io.StringIO(file_content),
sep=";",
@@ -308,68 +430,196 @@ def get_points_from_csv(file_content, current_user=None):
.astype(float)
)
df["time"] = pd.to_datetime(df["time"], format="%d.%m.%Y %H:%M:%S")
- for row in df.iterrows():
- row = row[1]
- match row["obj"].split(" ")[-1]:
- case "V":
- pol = "Вертикальная"
- case "H":
- pol = "Горизонтальная"
- case "R":
- pol = "Правая"
- case "L":
- pol = "Левая"
- case _:
- pol = "-"
- pol_obj, _ = Polarization.objects.get_or_create(name=pol)
- sat_obj, _ = Satellite.objects.get_or_create(
- name=row["sat"], defaults={"norad": row["norad_id"]}
- )
- mir_1_obj, _ = Mirror.objects.get_or_create(name=row["mir_1"])
- mir_2_obj, _ = Mirror.objects.get_or_create(name=row["mir_2"])
- mir_lst = [row["mir_1"], row["mir_2"]]
- if not pd.isna(row["mir_3"]):
- mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
- user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
- geo_obj, _ = Geo.objects.get_or_create(
- timestamp=row["time"],
- coords=Point(row["lon"], row["lat"], srid=4326),
- defaults={
- "is_average": False,
- # 'id_user_add': user_to_use,
- },
- )
- geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
+ # Шаг 1: Извлечь все координаты и данные строк из DataFrame
+ records = []
- # Check if ObjItem with same geo already exists
- existing_obj_item = ObjItem.objects.filter(geo_obj=geo_obj).first()
- if existing_obj_item:
- # Check if parameter with same values exists for this object
- if (
- hasattr(existing_obj_item, 'parameter_obj') and
- existing_obj_item.parameter_obj and
- existing_obj_item.parameter_obj.id_satellite == sat_obj and
- existing_obj_item.parameter_obj.polarization == pol_obj and
- existing_obj_item.parameter_obj.frequency == row["freq"] and
- existing_obj_item.parameter_obj.freq_range == row["f_range"]
- ):
- # Skip creating duplicate
- continue
+ for idx, row in df.iterrows():
+ try:
+ # Извлекаем координату из колонок lat и lon
+ coord_tuple = (row["lon"], row["lat"])
+
+ # Сохраняем запись с координатой и данными строки
+ records.append({"coord": coord_tuple, "row": row, "index": idx})
+ except Exception as e:
+ print(f"Ошибка при обработке строки {idx}: {e}")
+ continue
+
+ user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
+
+ # Шаг 3: Получить все существующие Source из БД
+ existing_sources = list(Source.objects.filter(coords_average__isnull=False))
+ new_sources_count = 0
+ added_count = 0
+ skipped_count = 0
+
+ # Шаг 4: Обработка каждой записи
+ for record in records:
+ current_coord = record["coord"]
+ row = record["row"]
- # Create new ObjItem and Parameter
- obj_item = ObjItem.objects.create(name=row["obj"], created_by=user_to_use)
+ # Шаг 4a: Проверить, существует ли дубликат (координаты + частота)
+ if _is_duplicate_objitem(current_coord, row["freq"], row["f_range"]):
+ skipped_count += 1
+ continue
- vch_load_obj = Parameter.objects.create(
- id_satellite=sat_obj,
- polarization=pol_obj,
- frequency=row["freq"],
- freq_range=row["f_range"],
- objitem=obj_item
- )
+ # Шаг 4c: Найти ближайший существующий Source
+ closest_source = None
+ min_distance = float('inf')
- geo_obj.objitem = obj_item
- geo_obj.save()
+ for source in existing_sources:
+ source_coord = (source.coords_average.x, source.coords_average.y)
+ distance = calculate_distance_degrees(source_coord, current_coord)
+
+ if distance < min_distance:
+ min_distance = distance
+ closest_source = source
+
+ # Шаг 4d: Если найден близкий Source (расстояние <= 0.5 градуса)
+ if closest_source and min_distance <= 0.5:
+ # Обновить coords_average инкрементально
+ current_avg = (closest_source.coords_average.x, closest_source.coords_average.y)
+ new_avg = calculate_average_coords_incremental(current_avg, current_coord)
+ closest_source.coords_average = Point(new_avg, srid=4326)
+ closest_source.save()
+
+ # Создать ObjItem и связать с существующим Source
+ _create_objitem_from_csv_row(row, closest_source, user_to_use)
+ added_count += 1
+ else:
+ # Шаг 4e: Создать новый Source
+ new_source = Source.objects.create(
+ coords_average=Point(current_coord, srid=4326),
+ created_by=user_to_use
+ )
+ new_sources_count += 1
+
+ # Создать ObjItem и связать с новым Source
+ _create_objitem_from_csv_row(row, new_source, user_to_use)
+ added_count += 1
+
+ # Добавить новый Source в список существующих
+ existing_sources.append(new_source)
+
+ print(f"Импорт завершен: создано {new_sources_count} новых источников, "
+ f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
+
+ return new_sources_count
+
+
+def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.001):
+ """
+ Проверяет, существует ли уже ObjItem с такими же координатами и частотой.
+
+ Args:
+ coord_tuple: кортеж (lon, lat) координат
+ frequency: частота в МГц
+ freq_range: полоса частот в МГц
+ tolerance: допуск для сравнения координат в градусах (по умолчанию 0.001 ≈ 100м)
+
+ Returns:
+ bool: True если дубликат найден, False иначе
+ """
+ # Ищем ObjItems с близкими координатами через geo_obj
+ nearby_objitems = ObjItem.objects.filter(
+ geo_obj__coords__isnull=False
+ ).select_related('parameter_obj', 'geo_obj')
+
+ for objitem in nearby_objitems:
+ if not objitem.geo_obj or not objitem.geo_obj.coords:
+ continue
+
+ # Проверяем расстояние между координатами
+ geo_coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
+ distance = calculate_distance_degrees(coord_tuple, geo_coord)
+
+ if distance <= tolerance:
+ # Координаты совпадают, проверяем частоту
+ if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
+ param = objitem.parameter_obj
+ # Проверяем совпадение частоты с небольшим допуском (0.1 МГц)
+ if (abs(param.frequency - frequency) < 0.1 and
+ abs(param.freq_range - freq_range) < 0.1):
+ return True
+
+ return False
+
+
+def _create_objitem_from_csv_row(row, source, user_to_use):
+ """
+ Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
+
+ Args:
+ row: строка DataFrame
+ source: объект Source для связи
+ user_to_use: пользователь для created_by
+ """
+ # Определяем поляризацию
+ match row["obj"].split(" ")[-1]:
+ case "V":
+ pol = "Вертикальная"
+ case "H":
+ pol = "Горизонтальная"
+ case "R":
+ pol = "Правая"
+ case "L":
+ pol = "Левая"
+ case _:
+ pol = "-"
+
+ pol_obj, _ = Polarization.objects.get_or_create(name=pol)
+ sat_obj, _ = Satellite.objects.get_or_create(
+ name=row["sat"], defaults={"norad": row["norad_id"]}
+ )
+ mir_1_obj, _ = Mirror.objects.get_or_create(name=row["mir_1"])
+ mir_2_obj, _ = Mirror.objects.get_or_create(name=row["mir_2"])
+ mir_lst = [row["mir_1"], row["mir_2"]]
+ if not pd.isna(row["mir_3"]):
+ mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
+ mir_lst.append(row["mir_3"])
+
+ # Создаем Geo объект (БЕЗ coords_kupsat и coords_valid)
+ geo_obj, _ = Geo.objects.get_or_create(
+ timestamp=row["time"],
+ coords=Point(row["lon"], row["lat"], srid=4326),
+ defaults={
+ "is_average": False,
+ },
+ )
+ geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
+
+ # Проверяем, существует ли уже ObjItem с таким же geo
+ existing_obj_item = ObjItem.objects.filter(geo_obj=geo_obj).first()
+ if existing_obj_item:
+ # Проверяем, существует ли parameter с такими же значениями
+ if (
+ hasattr(existing_obj_item, "parameter_obj")
+ and existing_obj_item.parameter_obj
+ and existing_obj_item.parameter_obj.id_satellite == sat_obj
+ and existing_obj_item.parameter_obj.polarization == pol_obj
+ and existing_obj_item.parameter_obj.frequency == row["freq"]
+ and existing_obj_item.parameter_obj.freq_range == row["f_range"]
+ ):
+ # Пропускаем создание дубликата
+ return
+
+ # Создаем новый ObjItem и связываем с Source
+ obj_item = ObjItem.objects.create(
+ name=row["obj"], source=source, created_by=user_to_use
+ )
+
+ # Создаем Parameter
+ Parameter.objects.create(
+ id_satellite=sat_obj,
+ polarization=pol_obj,
+ frequency=row["freq"],
+ freq_range=row["f_range"],
+ objitem=obj_item,
+ )
+
+ # Связываем geo с objitem
+ geo_obj.objitem = obj_item
+ geo_obj.save()
def get_vch_load_from_html(file, sat: Satellite) -> None:
@@ -450,13 +700,13 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
def get_frequency_tolerance_percent(freq_range_mhz: float) -> float:
"""
Определяет процент погрешности центральной частоты в зависимости от полосы частот.
-
+
Args:
freq_range_mhz (float): Полоса частот в МГц
-
+
Returns:
float: Процент погрешности для центральной частоты
-
+
Диапазоны:
- 0 - 0.5 МГц (0 - 500 кГц): 0.1%
- 0.5 - 1.5 МГц (500 кГц - 1.5 МГц): 0.5%
@@ -481,62 +731,66 @@ def compare_and_link_vch_load(
):
"""
Привязывает SigmaParameter к Parameter на основе совпадения параметров.
-
+
Погрешность центральной частоты определяется автоматически в зависимости от полосы частот:
- 0-500 кГц: 0.1%
- 500 кГц-1.5 МГц: 0.5%
- 1.5-5 МГц: 1%
- 5-10 МГц: 2%
- >10 МГц: 5%
-
+
Args:
sat_id (Satellite): Спутник для фильтрации
eps_freq (float): Не используется (оставлен для обратной совместимости)
eps_frange (float): Погрешность полосы частот в процентах
ku_range (float): Не используется (оставлен для обратной совместимости)
-
+
Returns:
tuple: (количество объектов, количество привязок)
"""
# Получаем все ObjItem с Parameter для данного спутника
item_obj = ObjItem.objects.filter(
parameter_obj__id_satellite=sat_id
- ).select_related('parameter_obj', 'parameter_obj__polarization')
-
- vch_sigma = SigmaParameter.objects.filter(
- id_satellite=sat_id
- ).select_related('polarization')
-
+ ).select_related("parameter_obj", "parameter_obj__polarization")
+
+ vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id).select_related(
+ "polarization"
+ )
+
link_count = 0
obj_count = item_obj.count()
-
+
for obj in item_obj:
vch_load = obj.parameter_obj
-
+
# Пропускаем объекты с некорректной частотой
if not vch_load or vch_load.frequency == -1.0:
continue
-
+
# Определяем погрешность частоты на основе полосы
freq_tolerance_percent = get_frequency_tolerance_percent(vch_load.freq_range)
-
+
# Вычисляем допустимое отклонение частоты в МГц
freq_tolerance_mhz = vch_load.frequency * freq_tolerance_percent / 100
-
+
# Вычисляем допустимое отклонение полосы в МГц
frange_tolerance_mhz = vch_load.freq_range * eps_frange / 100
-
+
for sigma in vch_sigma:
# Проверяем совпадение по всем параметрам
- freq_match = abs(sigma.transfer_frequency - vch_load.frequency) <= freq_tolerance_mhz
- frange_match = abs(sigma.freq_range - vch_load.freq_range) <= frange_tolerance_mhz
+ freq_match = (
+ abs(sigma.transfer_frequency - vch_load.frequency) <= freq_tolerance_mhz
+ )
+ frange_match = (
+ abs(sigma.freq_range - vch_load.freq_range) <= frange_tolerance_mhz
+ )
pol_match = sigma.polarization == vch_load.polarization
-
+
if freq_match and frange_match and pol_match:
sigma.parameter = vch_load
sigma.save()
link_count += 1
-
+
return obj_count, link_count
@@ -614,6 +868,92 @@ def kub_report(data_in: io.StringIO) -> pd.DataFrame:
return df
+# ============================================================================
+# Утилиты для работы с координатами
+# ============================================================================
+
+
+def calculate_distance_degrees(coord1: tuple, coord2: tuple) -> float:
+ """
+ Вычисляет расстояние между двумя координатами в градусах.
+
+ Использует простую евклидову метрику для малых расстояний.
+ Подходит для определения близости точек в радиусе до нескольких градусов.
+
+ Args:
+ coord1 (tuple): Первая координата в формате (longitude, latitude)
+ coord2 (tuple): Вторая координата в формате (longitude, latitude)
+
+ Returns:
+ float: Расстояние в градусах
+
+ Example:
+ >>> dist = calculate_distance_degrees((37.62, 55.75), (37.63, 55.76))
+ >>> print(f"{dist:.4f}") # ~0.0141 градусов
+ 0.0141
+
+ >>> dist = calculate_distance_degrees((37.62, 55.75), (37.62, 55.75))
+ >>> print(dist) # Одинаковые координаты
+ 0.0
+ """
+ lon1, lat1 = coord1
+ lon2, lat2 = coord2
+
+ # Простая евклидова метрика для малых расстояний
+ # Для более точных расчетов на больших расстояниях можно использовать формулу гаверсинуса
+ delta_lon = lon2 - lon1
+ delta_lat = lat2 - lat1
+
+ distance = (delta_lon**2 + delta_lat**2) ** 0.5
+
+ return distance
+
+
+def calculate_average_coords_incremental(
+ current_average: tuple, new_coord: tuple
+) -> tuple:
+ """
+ Вычисляет новое среднее между текущим средним и новой координатой.
+
+ Использует инкрементальное усреднение: каждая новая точка усредняется
+ с текущим средним, а не со всеми точками кластера. Это упрощенный подход,
+ где новое среднее = (текущее_среднее + новая_координата) / 2.
+
+ Важно: Это НЕ среднее арифметическое всех точек кластера, а инкрементальное
+ усреднение между двумя точками (текущим средним и новой точкой).
+
+ Args:
+ current_average (tuple): Текущее среднее в формате (longitude, latitude)
+ new_coord (tuple): Новая координата в формате (longitude, latitude)
+
+ Returns:
+ tuple: Новое среднее в формате (longitude, latitude)
+
+ Example:
+ >>> avg1 = (37.62, 55.75) # Первая точка
+ >>> avg2 = calculate_average_coords_incremental(avg1, (37.63, 55.76))
+ >>> print(avg2)
+ (37.625, 55.755)
+
+ >>> avg3 = calculate_average_coords_incremental(avg2, (37.64, 55.77))
+ >>> print(avg3)
+ (37.6325, 55.7625)
+
+ >>> # Проверка: среднее между одинаковыми точками
+ >>> avg = calculate_average_coords_incremental((37.62, 55.75), (37.62, 55.75))
+ >>> print(avg)
+ (37.62, 55.75)
+ """
+ current_lon, current_lat = current_average
+ new_lon, new_lat = new_coord
+
+ # Инкрементальное усреднение: (current + new) / 2
+ avg_lon = (current_lon + new_lon) / 2
+ avg_lat = (current_lat + new_lat) / 2
+
+ return (avg_lon, avg_lat)
+
+
# ============================================================================
# Утилиты для форматирования
# ============================================================================
diff --git a/dbapp/mainapp/views.py b/dbapp/mainapp/views.py
index 4018fc1..b62ac68 100644
--- a/dbapp/mainapp/views.py
+++ b/dbapp/mainapp/views.py
@@ -104,15 +104,6 @@ class ActionsPageView(View):
return render(request, "mainapp/login_required.html")
-class HomePageView(View):
- def get(self, request):
- if request.user.is_authenticated:
- # Redirect to objitem list if authenticated
- return redirect("mainapp:objitem_list")
- else:
- return render(request, "mainapp/login_required.html")
-
-
class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
template_name = "mainapp/add_data_from_excel.html"
form_class = LoadExcelData
@@ -558,6 +549,275 @@ class SigmaParameterDataAPIView(LoginRequiredMixin, View):
return JsonResponse({'error': str(e)}, status=500)
+class SourceObjItemsAPIView(LoginRequiredMixin, View):
+ """API для получения списка ObjItem, связанных с источником"""
+
+ def get(self, request, source_id):
+ from .models import Source
+
+ try:
+ # Загружаем Source с prefetch_related для ObjItem
+ source = Source.objects.prefetch_related(
+ 'source_objitems',
+ 'source_objitems__parameter_obj',
+ 'source_objitems__parameter_obj__id_satellite',
+ 'source_objitems__parameter_obj__polarization',
+ 'source_objitems__parameter_obj__modulation',
+ 'source_objitems__geo_obj'
+ ).get(id=source_id)
+
+ # Получаем все связанные ObjItem, отсортированные по created_at
+ objitems = source.source_objitems.all().order_by('created_at')
+
+ objitems_data = []
+ for objitem in objitems:
+ # Получаем данные параметра
+ param = getattr(objitem, 'parameter_obj', None)
+ satellite_name = '-'
+ frequency = '-'
+ freq_range = '-'
+ polarization = '-'
+ bod_velocity = '-'
+ modulation = '-'
+ snr = '-'
+
+ if param:
+ if hasattr(param, 'id_satellite') and param.id_satellite:
+ satellite_name = param.id_satellite.name
+ frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-'
+ freq_range = f"{param.freq_range:.3f}" if param.freq_range is not None else '-'
+ if hasattr(param, 'polarization') and param.polarization:
+ polarization = param.polarization.name
+ bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else '-'
+ if hasattr(param, 'modulation') and param.modulation:
+ modulation = param.modulation.name
+ snr = f"{param.snr:.0f}" if param.snr is not None else '-'
+
+ # Получаем геоданные
+ geo_timestamp = '-'
+ geo_location = '-'
+ geo_coords = '-'
+
+ if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
+ if objitem.geo_obj.timestamp:
+ local_time = timezone.localtime(objitem.geo_obj.timestamp)
+ geo_timestamp = local_time.strftime("%d.%m.%Y %H:%M")
+
+ geo_location = objitem.geo_obj.location or '-'
+
+ if objitem.geo_obj.coords:
+ longitude = objitem.geo_obj.coords.coords[0]
+ latitude = objitem.geo_obj.coords.coords[1]
+ lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
+ lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
+ geo_coords = f"{lat} {lon}"
+
+ objitems_data.append({
+ 'id': objitem.id,
+ 'name': objitem.name or '-',
+ 'satellite_name': satellite_name,
+ 'frequency': frequency,
+ 'freq_range': freq_range,
+ 'polarization': polarization,
+ 'bod_velocity': bod_velocity,
+ 'modulation': modulation,
+ 'snr': snr,
+ 'geo_timestamp': geo_timestamp,
+ 'geo_location': geo_location,
+ 'geo_coords': geo_coords
+ })
+
+ return JsonResponse({
+ 'source_id': source_id,
+ 'objitems': objitems_data
+ })
+ except Source.DoesNotExist:
+ return JsonResponse({'error': 'Источник не найден'}, status=404)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
+
+
+class SourceListView(LoginRequiredMixin, View):
+ """
+ Представление для отображения списка источников (Source).
+ """
+
+ def get(self, request):
+ from .models import Source
+ from django.db.models import Count
+ from datetime import datetime
+
+ # Получаем параметры пагинации
+ page_number, items_per_page = parse_pagination_params(request)
+
+ # Получаем параметры сортировки
+ sort_param = request.GET.get("sort", "-created_at")
+
+ # Получаем параметры фильтров
+ search_query = request.GET.get("search", "").strip()
+ has_coords_average = request.GET.get("has_coords_average")
+ has_coords_kupsat = request.GET.get("has_coords_kupsat")
+ has_coords_valid = request.GET.get("has_coords_valid")
+ has_coords_reference = request.GET.get("has_coords_reference")
+ objitem_count_min = request.GET.get("objitem_count_min", "").strip()
+ objitem_count_max = request.GET.get("objitem_count_max", "").strip()
+ date_from = request.GET.get("date_from", "").strip()
+ date_to = request.GET.get("date_to", "").strip()
+
+ # Получаем все Source объекты с оптимизацией запросов
+ sources = Source.objects.select_related(
+ 'created_by__user',
+ 'updated_by__user'
+ ).prefetch_related(
+ 'source_objitems',
+ 'source_objitems__parameter_obj',
+ 'source_objitems__geo_obj'
+ ).annotate(
+ objitem_count=Count('source_objitems')
+ )
+
+ # Применяем фильтры
+ # Фильтр по наличию coords_average
+ if has_coords_average == "1":
+ sources = sources.filter(coords_average__isnull=False)
+ elif has_coords_average == "0":
+ sources = sources.filter(coords_average__isnull=True)
+
+ # Фильтр по наличию coords_kupsat
+ if has_coords_kupsat == "1":
+ sources = sources.filter(coords_kupsat__isnull=False)
+ elif has_coords_kupsat == "0":
+ sources = sources.filter(coords_kupsat__isnull=True)
+
+ # Фильтр по наличию coords_valid
+ if has_coords_valid == "1":
+ sources = sources.filter(coords_valid__isnull=False)
+ elif has_coords_valid == "0":
+ sources = sources.filter(coords_valid__isnull=True)
+
+ # Фильтр по наличию coords_reference
+ if has_coords_reference == "1":
+ sources = sources.filter(coords_reference__isnull=False)
+ elif has_coords_reference == "0":
+ sources = sources.filter(coords_reference__isnull=True)
+
+ # Фильтр по количеству ObjItem
+ if objitem_count_min:
+ try:
+ min_count = int(objitem_count_min)
+ sources = sources.filter(objitem_count__gte=min_count)
+ except ValueError:
+ pass
+
+ if objitem_count_max:
+ try:
+ max_count = int(objitem_count_max)
+ sources = sources.filter(objitem_count__lte=max_count)
+ except ValueError:
+ pass
+
+ # Фильтр по диапазону дат создания
+ if date_from:
+ try:
+ date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
+ sources = sources.filter(created_at__gte=date_from_obj)
+ except (ValueError, TypeError):
+ pass
+
+ if date_to:
+ try:
+ from datetime import timedelta
+ date_to_obj = datetime.strptime(date_to, "%Y-%m-%d")
+ # Добавляем один день чтобы включить весь конечный день
+ date_to_obj = date_to_obj + timedelta(days=1)
+ sources = sources.filter(created_at__lt=date_to_obj)
+ except (ValueError, TypeError):
+ pass
+
+ # Поиск по ID
+ if search_query:
+ try:
+ search_id = int(search_query)
+ sources = sources.filter(id=search_id)
+ except ValueError:
+ # Если не число, игнорируем
+ pass
+
+ # Применяем сортировку
+ valid_sort_fields = {
+ "id": "id",
+ "-id": "-id",
+ "created_at": "created_at",
+ "-created_at": "-created_at",
+ "updated_at": "updated_at",
+ "-updated_at": "-updated_at",
+ "objitem_count": "objitem_count",
+ "-objitem_count": "-objitem_count",
+ }
+
+ if sort_param in valid_sort_fields:
+ sources = sources.order_by(valid_sort_fields[sort_param])
+
+ # Создаем пагинатор
+ paginator = Paginator(sources, items_per_page)
+ page_obj = paginator.get_page(page_number)
+
+ # Подготавливаем данные для отображения
+ processed_sources = []
+ for source in page_obj:
+ # Форматируем координаты
+ def format_coords(point):
+ if point:
+ longitude = point.coords[0]
+ latitude = point.coords[1]
+ lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
+ lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
+ return f"{lat} {lon}"
+ return "-"
+
+ coords_average_str = format_coords(source.coords_average)
+ coords_kupsat_str = format_coords(source.coords_kupsat)
+ coords_valid_str = format_coords(source.coords_valid)
+ coords_reference_str = format_coords(source.coords_reference)
+
+ # Получаем количество связанных ObjItem
+ objitem_count = source.objitem_count
+
+ processed_sources.append({
+ 'id': source.id,
+ 'coords_average': coords_average_str,
+ 'coords_kupsat': coords_kupsat_str,
+ 'coords_valid': coords_valid_str,
+ 'coords_reference': coords_reference_str,
+ 'objitem_count': objitem_count,
+ 'created_at': source.created_at,
+ 'updated_at': source.updated_at,
+ 'created_by': source.created_by,
+ 'updated_by': source.updated_by,
+ })
+
+ # Подготавливаем контекст для шаблона
+ context = {
+ 'page_obj': page_obj,
+ 'processed_sources': processed_sources,
+ 'items_per_page': items_per_page,
+ 'available_items_per_page': [50, 100, 500, 1000],
+ 'sort': sort_param,
+ 'search_query': search_query,
+ 'has_coords_average': has_coords_average,
+ 'has_coords_kupsat': has_coords_kupsat,
+ 'has_coords_valid': has_coords_valid,
+ 'has_coords_reference': has_coords_reference,
+ 'objitem_count_min': objitem_count_min,
+ 'objitem_count_max': objitem_count_max,
+ 'date_from': date_from,
+ 'date_to': date_to,
+ 'full_width_page': True,
+ }
+
+ return render(request, "mainapp/source_list.html", context)
+
+
class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
template_name = "mainapp/process_kubsat.html"
form_class = NewEventForm