diff --git a/dbapp/dbapp/urls.py b/dbapp/dbapp/urls.py
index e33f309..6a9a9d7 100644
--- a/dbapp/dbapp/urls.py
+++ b/dbapp/dbapp/urls.py
@@ -16,7 +16,7 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import path, include
-from mainapp import views
+from mainapp.views import custom_logout
from django.contrib.auth import views as auth_views
from debug_toolbar.toolbar import debug_toolbar_urls
@@ -26,5 +26,5 @@ urlpatterns = [
path('', include('mapsapp.urls', namespace='mapsapp')),
# Authentication URLs
path('login/', auth_views.LoginView.as_view(), name='login'),
- path('logout/', views.custom_logout, name='logout'),
+ path('logout/', custom_logout, name='logout'),
] + debug_toolbar_urls()
diff --git a/dbapp/mainapp/mixins.py b/dbapp/mainapp/mixins.py
index c81150f..2160b70 100644
--- a/dbapp/mainapp/mixins.py
+++ b/dbapp/mainapp/mixins.py
@@ -1,229 +1,209 @@
-"""
-Переиспользуемые миксины для представлений mainapp.
-
-Этот модуль содержит миксины для стандартизации общей логики в представлениях,
-включая проверку прав доступа, обработку координат и сообщений.
-"""
-
-# Standard library imports
-from datetime import datetime
-from typing import Optional, Tuple
-
-# Django imports
-from django.contrib import messages
-from django.contrib.auth.mixins import UserPassesTestMixin
-from django.contrib.gis.geos import Point
-
-
-class RoleRequiredMixin(UserPassesTestMixin):
- """
- Mixin для проверки роли пользователя.
-
- Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
-
- Attributes:
- required_roles (list): Список допустимых ролей для доступа.
- По умолчанию ['admin', 'moderator'].
-
- Example:
- class MyView(RoleRequiredMixin, View):
- required_roles = ['admin', 'moderator']
-
- def get(self, request):
- # Только пользователи с ролью admin или moderator могут получить доступ
- return render(request, 'template.html')
- """
-
- required_roles = ["admin", "moderator"]
-
- def test_func(self) -> bool:
- """
- Проверяет, имеет ли пользователь требуемую роль.
-
- Returns:
- bool: True если пользователь имеет одну из требуемых ролей, иначе False.
- """
- if not self.request.user.is_authenticated:
- return False
-
- if not hasattr(self.request.user, "customuser"):
- return False
-
- return self.request.user.customuser.role in self.required_roles
-
-
-class CoordinateProcessingMixin:
- """
- Mixin для обработки координат из POST данных форм.
-
- Предоставляет методы для извлечения и обработки координат различных типов
- (геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
-
- Example:
- class MyFormView(CoordinateProcessingMixin, FormView):
- def form_valid(self, form):
- geo_instance = Geo()
- self.process_coordinates(geo_instance)
- geo_instance.save()
- return super().form_valid(form)
- """
-
- def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
- """
- Обрабатывает координаты из POST данных и применяет их к объекту Geo.
-
- Извлекает координаты геолокации, кубсата и оперативников из POST запроса
- и устанавливает соответствующие поля объекта Geo.
-
- Args:
- geo_instance: Экземпляр модели Geo для обновления координат.
- prefix (str): Префикс для полей формы (по умолчанию 'geo').
-
- Note:
- Метод ожидает следующие поля в request.POST:
- - geo_longitude, geo_latitude: координаты геолокации
- - kupsat_longitude, kupsat_latitude: координаты кубсата
- - valid_longitude, valid_latitude: координаты оперативников
- """
- # Обрабатываем координаты геолокации
- geo_coords = self._extract_coordinates("geo")
- if geo_coords:
- geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
-
- # Обрабатываем координаты Кубсата
- kupsat_coords = self._extract_coordinates("kupsat")
- if kupsat_coords:
- geo_instance.coords_kupsat = Point(
- kupsat_coords[0], kupsat_coords[1], srid=4326
- )
-
- # Обрабатываем координаты оперативников
- valid_coords = self._extract_coordinates("valid")
- if valid_coords:
- geo_instance.coords_valid = Point(
- valid_coords[0], valid_coords[1], srid=4326
- )
-
- def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
- """
- Извлекает координаты указанного типа из POST данных.
-
- Args:
- coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
-
- Returns:
- Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
- если координаты не найдены или невалидны.
- """
- longitude_key = f"{coord_type}_longitude"
- latitude_key = f"{coord_type}_latitude"
-
- longitude = self.request.POST.get(longitude_key)
- latitude = self.request.POST.get(latitude_key)
-
- if longitude and latitude:
- try:
- return (float(longitude), float(latitude))
- except (ValueError, TypeError):
- return None
- return None
-
- def process_timestamp(self, geo_instance) -> None:
- """
- Обрабатывает дату и время из POST данных и применяет к объекту Geo.
-
- Args:
- geo_instance: Экземпляр модели Geo для обновления timestamp.
-
- Note:
- Метод ожидает следующие поля в request.POST:
- - timestamp_date: дата в формате YYYY-MM-DD
- - timestamp_time: время в формате HH:MM
- """
- timestamp_date = self.request.POST.get("timestamp_date")
- timestamp_time = self.request.POST.get("timestamp_time")
-
- if timestamp_date and timestamp_time:
- try:
- naive_datetime = datetime.strptime(
- f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
- )
- geo_instance.timestamp = naive_datetime
- except ValueError:
- # Если формат даты/времени неверный, пропускаем
- pass
-
-
-class FormMessageMixin:
- """
- Mixin для стандартизации сообщений об успехе и ошибках в формах.
-
- Автоматически добавляет сообщения пользователю при успешной или неуспешной
- обработке формы.
-
- Attributes:
- success_message (str): Сообщение при успешной обработке формы.
- error_message (str): Сообщение при ошибке обработки формы.
-
- Example:
- class MyFormView(FormMessageMixin, FormView):
- success_message = "Данные успешно сохранены!"
- error_message = "Ошибка при сохранении данных"
-
- def form_valid(self, form):
- # Автоматически добавит success_message
- return super().form_valid(form)
- """
-
- success_message = "Операция выполнена успешно"
- error_message = "Произошла ошибка при обработке формы"
-
- def form_valid(self, form):
- """
- Обрабатывает валидную форму и добавляет сообщение об успехе.
-
- Args:
- form: Валидная форма Django.
-
- Returns:
- HttpResponse: Результат обработки родительского метода form_valid.
- """
- if self.success_message:
- messages.success(self.request, self.success_message)
- return super().form_valid(form)
-
- def form_invalid(self, form):
- """
- Обрабатывает невалидную форму и добавляет сообщение об ошибке.
-
- Args:
- form: Невалидная форма Django.
-
- Returns:
- HttpResponse: Результат обработки родительского метода form_invalid.
- """
- if self.error_message:
- messages.error(self.request, self.error_message)
- return super().form_invalid(form)
-
- def get_success_message(self) -> str:
- """
- Возвращает сообщение об успехе.
-
- Может быть переопределен в подклассах для динамического формирования сообщения.
-
- Returns:
- str: Сообщение об успехе.
- """
- return self.success_message
-
- def get_error_message(self) -> str:
- """
- Возвращает сообщение об ошибке.
-
- Может быть переопределен в подклассах для динамического формирования сообщения.
-
- Returns:
- str: Сообщение об ошибке.
- """
- return self.error_message
+"""
+Переиспользуемые миксины для представлений mainapp.
+
+Этот модуль содержит миксины для стандартизации общей логики в представлениях,
+включая проверку прав доступа, обработку координат и сообщений.
+"""
+
+# Standard library imports
+from datetime import datetime
+from typing import Optional, Tuple
+
+# Django imports
+from django.contrib import messages
+from django.contrib.auth.mixins import UserPassesTestMixin
+from django.contrib.gis.geos import Point
+
+
+class RoleRequiredMixin(UserPassesTestMixin):
+ """
+ Mixin для проверки роли пользователя.
+
+ Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
+
+ Attributes:
+ required_roles (list): Список допустимых ролей для доступа.
+ По умолчанию ['admin', 'moderator'].
+
+ Example:
+ class MyView(RoleRequiredMixin, View):
+ required_roles = ['admin', 'moderator']
+
+ def get(self, request):
+ # Только пользователи с ролью admin или moderator могут получить доступ
+ return render(request, 'template.html')
+ """
+
+ required_roles = ["admin", "moderator"]
+
+ def test_func(self) -> bool:
+ """
+ Проверяет, имеет ли пользователь требуемую роль.
+
+ Returns:
+ bool: True если пользователь имеет одну из требуемых ролей, иначе False.
+ """
+ if not self.request.user.is_authenticated:
+ return False
+
+ if not hasattr(self.request.user, "customuser"):
+ return False
+
+ return self.request.user.customuser.role in self.required_roles
+
+
+class CoordinateProcessingMixin:
+ """
+ Mixin для обработки координат из POST данных форм.
+
+ Предоставляет методы для извлечения и обработки координат различных типов
+ (геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
+
+ Note: Координаты Кубсата и оперативников теперь хранятся в модели Source,
+ а не в модели Geo, но для совместимости в форме все еще могут быть поля
+ для этих координат.
+ """
+
+ def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
+ """
+ Обрабатывает координаты из POST данных и применяет их к объекту Geo.
+
+ Извлекает координаты геолокации из POST запроса
+ и устанавливает соответствующие поля объекта Geo.
+
+ Args:
+ geo_instance: Экземпляр модели Geo для обновления координат.
+ prefix (str): Префикс для полей формы (по умолчанию 'geo').
+
+ Note:
+ Метод ожидает следующие поля в request.POST:
+ - geo_longitude, geo_latitude: координаты геолокации
+ """
+ # Обрабатываем координаты геолокации
+ geo_coords = self._extract_coordinates("geo")
+ if geo_coords:
+ geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
+
+ def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
+ """
+ Извлекает координаты указанного типа из POST данных.
+
+ Args:
+ coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
+
+ Returns:
+ Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
+ если координаты не найдены или невалидны.
+ """
+ longitude_key = f"{coord_type}_longitude"
+ latitude_key = f"{coord_type}_latitude"
+
+ longitude = self.request.POST.get(longitude_key)
+ latitude = self.request.POST.get(latitude_key)
+
+ if longitude and latitude:
+ try:
+ return (float(longitude), float(latitude))
+ except (ValueError, TypeError):
+ return None
+ return None
+
+ def process_timestamp(self, geo_instance) -> None:
+ """
+ Обрабатывает дату и время из POST данных и применяет к объекту Geo.
+
+ Args:
+ geo_instance: Экземпляр модели Geo для обновления timestamp.
+
+ Note:
+ Метод ожидает следующие поля в request.POST:
+ - timestamp_date: дата в формате YYYY-MM-DD
+ - timestamp_time: время в формате HH:MM
+ """
+ timestamp_date = self.request.POST.get("timestamp_date")
+ timestamp_time = self.request.POST.get("timestamp_time")
+
+ if timestamp_date and timestamp_time:
+ try:
+ naive_datetime = datetime.strptime(
+ f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
+ )
+ geo_instance.timestamp = naive_datetime
+ except ValueError:
+ # Если формат даты/времени неверный, пропускаем
+ pass
+
+
+class FormMessageMixin:
+ """
+ Mixin для стандартизации сообщений об успехе и ошибках в формах.
+
+ Автоматически добавляет сообщения пользователю при успешной или неуспешной
+ обработке формы.
+
+ Attributes:
+ success_message (str): Сообщение при успешной обработке формы.
+ error_message (str): Сообщение при ошибке обработки формы.
+
+ Example:
+ class MyFormView(FormMessageMixin, FormView):
+ success_message = "Данные успешно сохранены!"
+ error_message = "Ошибка при сохранении данных"
+
+ def form_valid(self, form):
+ # Автоматически добавит success_message
+ return super().form_valid(form)
+ """
+
+ success_message = "Операция выполнена успешно"
+ error_message = "Произошла ошибка при обработке формы"
+
+ def form_valid(self, form):
+ """
+ Обрабатывает валидную форму и добавляет сообщение об успехе.
+
+ Args:
+ form: Валидная форма Django.
+
+ Returns:
+ HttpResponse: Результат обработки родительского метода form_valid.
+ """
+ if self.success_message:
+ messages.success(self.request, self.success_message)
+ return super().form_valid(form)
+
+ def form_invalid(self, form):
+ """
+ Обрабатывает невалидную форму и добавляет сообщение об ошибке.
+
+ Args:
+ form: Невалидная форма Django.
+
+ Returns:
+ HttpResponse: Результат обработки родительского метода form_invalid.
+ """
+ if self.error_message:
+ messages.error(self.request, self.error_message)
+ return super().form_invalid(form)
+
+ def get_success_message(self) -> str:
+ """
+ Возвращает сообщение об успехе.
+
+ Может быть переопределен в подклассах для динамического формирования сообщения.
+
+ Returns:
+ str: Сообщение об успехе.
+ """
+ return self.success_message
+
+ def get_error_message(self) -> str:
+ """
+ Возвращает сообщение об ошибке.
+
+ Может быть переопределен в подклассах для динамического формирования сообщения.
+
+ Returns:
+ str: Сообщение об ошибке.
+ """
+ return self.error_message
diff --git a/dbapp/mainapp/templates/mainapp/objitem_detail.html b/dbapp/mainapp/templates/mainapp/objitem_detail.html
index c7438d2..926a26f 100644
--- a/dbapp/mainapp/templates/mainapp/objitem_detail.html
+++ b/dbapp/mainapp/templates/mainapp/objitem_detail.html
@@ -1,473 +1,319 @@
-{% extends 'mainapp/base.html' %}
-{% load static %}
-{% load static leaflet_tags %}
-{% load l10n %}
-
-{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
-
-{% block extra_css %}
-
-{% endblock %}
-
-{% block content %}
-
-
-
-
Просмотр объекта: {{ object.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
-
Редактировать
- {% endif %}
-
Назад
-
-
-{% endblock %}
-
-{% block extra_js %}
-{{ block.super }}
-
-{% leaflet_js %}
-{% leaflet_css %}
-
-
-
+{% extends 'mainapp/base.html' %}
+{% load static %}
+{% load static leaflet_tags %}
+{% load l10n %}
+
+{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
Просмотр объекта: {{ object.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
+
Редактировать
+ {% endif %}
+
Назад
+
+
+{% endblock %}
+
+{% block extra_js %}
+{{ block.super }}
+
+{% leaflet_js %}
+{% leaflet_css %}
+
+
+
{% endblock %}
\ No newline at end of file
diff --git a/dbapp/mainapp/templates/mainapp/objitem_form.html b/dbapp/mainapp/templates/mainapp/objitem_form.html
index f282f35..c406150 100644
--- a/dbapp/mainapp/templates/mainapp/objitem_form.html
+++ b/dbapp/mainapp/templates/mainapp/objitem_form.html
@@ -1,603 +1,448 @@
-{% extends 'mainapp/base.html' %}
-{% load static %}
-{% load static leaflet_tags %}
-{% load l10n %}
-
-{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
-
-{% block extra_css %}
-
-{% endblock %}
-
-{% block content %}
-
-
-
-
{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}
-
- {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
-
Сохранить
- {% if object %}
-
Удалить
- {% endif %}
- {% endif %}
-
Назад
-
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-{{ block.super }}
-
-{% leaflet_js %}
-{% leaflet_css %}
-
-
-
-
+{% extends 'mainapp/base.html' %}
+{% load static %}
+{% load static leaflet_tags %}
+{% load l10n %}
+
+{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}
+
+ {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
+
Сохранить
+ {% if object %}
+
Удалить
+ {% endif %}
+ {% endif %}
+
Назад
+
+
+
+
+
+
+{% endblock %}
+
+{% block extra_js %}
+{{ block.super }}
+
+{% leaflet_js %}
+{% leaflet_css %}
+
+
+
+
{% endblock %}
\ No newline at end of file
diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py
index 45b1a7e..902155d 100644
--- a/dbapp/mainapp/urls.py
+++ b/dbapp/mainapp/urls.py
@@ -1,37 +1,67 @@
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
-from . import views
+from .views import (
+ ActionsPageView,
+ AddSatellitesView,
+ AddTranspondersView,
+ ClusterTestView,
+ ClearLyngsatCacheView,
+ DeleteSelectedObjectsView,
+ FillLyngsatDataView,
+ GetLocationsView,
+ LinkLyngsatSourcesView,
+ LinkVchSigmaView,
+ LoadCsvDataView,
+ LoadExcelDataView,
+ LyngsatDataAPIView,
+ LyngsatTaskStatusAPIView,
+ LyngsatTaskStatusView,
+ ObjItemCreateView,
+ ObjItemDeleteView,
+ ObjItemDetailView,
+ ObjItemListView,
+ ObjItemUpdateView,
+ ProcessKubsatView,
+ ShowMapView,
+ ShowSelectedObjectsMapView,
+ SourceListView,
+ SourceObjItemsAPIView,
+ SigmaParameterDataAPIView,
+ UploadVchLoadView,
+ custom_logout,
+)
app_name = 'mainapp'
urlpatterns = [
- path('', views.SourceListView.as_view(), name='home'),
- path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'),
- path('actions/', views.ActionsPageView.as_view(), name='actions'),
- path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
- path('satellites', views.AddSatellitesView.as_view(), name='add_sats'),
- path('api/locations//geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
- path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
- path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
- path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
- path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
- path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
- path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
- path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
- path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
- 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'),
- path('object//', views.ObjItemDetailView.as_view(), name='objitem_detail'),
- path('object//delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
- path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
- path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
- path('lyngsat-task-status//', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
- path('api/lyngsat-task-status//', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
- path('clear-lyngsat-cache/', views.ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
+ path('', SourceListView.as_view(), name='home'),
+ path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
+ path('actions/', ActionsPageView.as_view(), name='actions'),
+ path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
+ path('satellites', AddSatellitesView.as_view(), name='add_sats'),
+ path('api/locations//geojson/', GetLocationsView.as_view(), name='locations_by_id'),
+ path('transponders', AddTranspondersView.as_view(), name='add_trans'),
+ path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
+ path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
+ path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
+ path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
+ path('cluster/', ClusterTestView.as_view(), name='cluster'),
+ path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
+ path('vch-link/', LinkVchSigmaView.as_view(), name='link_vch_sigma'),
+ path('link-lyngsat/', LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
+ path('api/lyngsat//', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
+ path('api/sigma-parameter//', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
+ path('api/source//objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
+ path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
+ path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
+ path('object//edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
+ path('object//', ObjItemDetailView.as_view(), name='objitem_detail'),
+ path('object//delete/', ObjItemDeleteView.as_view(), name='objitem_delete'),
+ path('fill-lyngsat-data/', FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
+ path('lyngsat-task-status/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
+ path('lyngsat-task-status//', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
+ path('api/lyngsat-task-status//', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
+ path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
+ path('logout/', custom_logout, name='logout'),
]
\ No newline at end of file
diff --git a/dbapp/mainapp/views/README.md b/dbapp/mainapp/views/README.md
new file mode 100644
index 0000000..c29d148
--- /dev/null
+++ b/dbapp/mainapp/views/README.md
@@ -0,0 +1,75 @@
+# Views Module Structure
+
+This directory contains the refactored views from the original monolithic `views.py` file.
+
+## File Organization
+
+### `__init__.py`
+Central import file that exports all views for easy access. This allows other modules to import views using:
+```python
+from mainapp.views import ObjItemListView, custom_logout
+```
+
+### `base.py`
+Basic views and utilities:
+- `ActionsPageView` - Displays the actions page
+- `custom_logout()` - Custom logout function
+
+### `objitem.py`
+ObjItem CRUD operations and related views:
+- `ObjItemListView` - List view with filtering and pagination
+- `ObjItemFormView` - Base class for create/update operations
+- `ObjItemCreateView` - Create new ObjItem
+- `ObjItemUpdateView` - Update existing ObjItem
+- `ObjItemDeleteView` - Delete ObjItem
+- `ObjItemDetailView` - Read-only detail view
+- `DeleteSelectedObjectsView` - Bulk delete operation
+
+### `data_import.py`
+Data import views for various formats:
+- `AddSatellitesView` - Add satellites to database
+- `AddTranspondersView` - Upload and parse transponder data from XML
+- `LoadExcelDataView` - Load data from Excel files
+- `LoadCsvDataView` - Load data from CSV files
+- `UploadVchLoadView` - Upload VCH load data from HTML
+- `LinkVchSigmaView` - Link VCH data with Sigma parameters
+- `ProcessKubsatView` - Process Kubsat event data
+
+### `api.py`
+API endpoints for AJAX requests:
+- `GetLocationsView` - Get locations by satellite ID in GeoJSON format
+- `LyngsatDataAPIView` - Get LyngSat source data
+- `SigmaParameterDataAPIView` - Get SigmaParameter data
+- `SourceObjItemsAPIView` - Get ObjItems related to a Source
+- `LyngsatTaskStatusAPIView` - Get Celery task status
+
+### `lyngsat.py`
+LyngSat related views:
+- `LinkLyngsatSourcesView` - Link LyngSat sources to objects
+- `FillLyngsatDataView` - Fill data from Lyngsat website
+- `LyngsatTaskStatusView` - Track Lyngsat data filling task status
+- `ClearLyngsatCacheView` - Clear LyngSat cache
+
+### `source.py`
+Source related views:
+- `SourceListView` - List view for Source objects with filtering
+
+### `map.py`
+Map related views:
+- `ShowMapView` - Display objects on map (admin interface)
+- `ShowSelectedObjectsMapView` - Display selected objects on map
+- `ClusterTestView` - Test view for clustering functionality
+
+## Migration Notes
+
+The original `views.py` has been renamed to `views_old.py` as a backup. All imports have been updated in:
+- `dbapp/mainapp/urls.py`
+- `dbapp/dbapp/urls.py`
+
+## Benefits of This Structure
+
+1. **Better Organization** - Related views are grouped together
+2. **Easier Maintenance** - Smaller files are easier to navigate and modify
+3. **Clear Responsibilities** - Each file has a specific purpose
+4. **Improved Testability** - Easier to write focused unit tests
+5. **Better Collaboration** - Multiple developers can work on different files without conflicts
diff --git a/dbapp/mainapp/views/__init__.py b/dbapp/mainapp/views/__init__.py
new file mode 100644
index 0000000..6b829e9
--- /dev/null
+++ b/dbapp/mainapp/views/__init__.py
@@ -0,0 +1,72 @@
+# Import all views for easy access
+from .base import ActionsPageView, custom_logout
+from .objitem import (
+ ObjItemListView,
+ ObjItemCreateView,
+ ObjItemUpdateView,
+ ObjItemDeleteView,
+ ObjItemDetailView,
+ DeleteSelectedObjectsView,
+)
+from .data_import import (
+ AddSatellitesView,
+ AddTranspondersView,
+ LoadExcelDataView,
+ LoadCsvDataView,
+ UploadVchLoadView,
+ LinkVchSigmaView,
+ ProcessKubsatView,
+)
+from .api import (
+ GetLocationsView,
+ LyngsatDataAPIView,
+ SigmaParameterDataAPIView,
+ SourceObjItemsAPIView,
+ LyngsatTaskStatusAPIView,
+)
+from .lyngsat import (
+ LinkLyngsatSourcesView,
+ FillLyngsatDataView,
+ LyngsatTaskStatusView,
+ ClearLyngsatCacheView,
+)
+from .source import SourceListView
+from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView
+
+__all__ = [
+ # Base
+ 'ActionsPageView',
+ 'custom_logout',
+ # ObjItem
+ 'ObjItemListView',
+ 'ObjItemCreateView',
+ 'ObjItemUpdateView',
+ 'ObjItemDeleteView',
+ 'ObjItemDetailView',
+ 'DeleteSelectedObjectsView',
+ # Data Import
+ 'AddSatellitesView',
+ 'AddTranspondersView',
+ 'LoadExcelDataView',
+ 'LoadCsvDataView',
+ 'UploadVchLoadView',
+ 'LinkVchSigmaView',
+ 'ProcessKubsatView',
+ # API
+ 'GetLocationsView',
+ 'LyngsatDataAPIView',
+ 'SigmaParameterDataAPIView',
+ 'SourceObjItemsAPIView',
+ 'LyngsatTaskStatusAPIView',
+ # LyngSat
+ 'LinkLyngsatSourcesView',
+ 'FillLyngsatDataView',
+ 'LyngsatTaskStatusView',
+ 'ClearLyngsatCacheView',
+ # Source
+ 'SourceListView',
+ # Map
+ 'ShowMapView',
+ 'ShowSelectedObjectsMapView',
+ 'ClusterTestView',
+]
diff --git a/dbapp/mainapp/views/api.py b/dbapp/mainapp/views/api.py
new file mode 100644
index 0000000..dd54e87
--- /dev/null
+++ b/dbapp/mainapp/views/api.py
@@ -0,0 +1,301 @@
+"""
+API endpoints for AJAX requests and data retrieval.
+"""
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import JsonResponse
+from django.utils import timezone
+from django.views import View
+
+from ..models import ObjItem
+
+
+class GetLocationsView(LoginRequiredMixin, View):
+ """API endpoint for getting locations by satellite ID in GeoJSON format."""
+
+ def get(self, request, sat_id):
+ locations = (
+ ObjItem.objects.filter(parameter_obj__id_satellite=sat_id)
+ .select_related(
+ "geo_obj",
+ "parameter_obj",
+ "parameter_obj__polarization",
+ )
+ )
+
+ if not locations.exists():
+ return JsonResponse({"error": "Объектов не найдено"}, status=404)
+
+ features = []
+ for loc in locations:
+ if not hasattr(loc, "geo_obj") or not loc.geo_obj or not loc.geo_obj.coords:
+ continue
+
+ param = getattr(loc, 'parameter_obj', None)
+ if not param:
+ continue
+
+ features.append(
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [loc.geo_obj.coords[0], loc.geo_obj.coords[1]],
+ },
+ "properties": {
+ "pol": param.polarization.name if param.polarization else "-",
+ "freq": param.frequency * 1000000 if param.frequency else 0,
+ "name": loc.name or "-",
+ "id": loc.geo_obj.id,
+ },
+ }
+ )
+
+ return JsonResponse({"type": "FeatureCollection", "features": features})
+
+
+class LyngsatDataAPIView(LoginRequiredMixin, View):
+ """API endpoint for getting LyngSat source data."""
+
+ def get(self, request, lyngsat_id):
+ from lyngsatapp.models import LyngSat
+
+ try:
+ lyngsat = LyngSat.objects.select_related(
+ 'id_satellite',
+ 'polarization',
+ 'modulation',
+ 'standard'
+ ).get(id=lyngsat_id)
+
+ # Format date with local timezone
+ last_update_str = '-'
+ if lyngsat.last_update:
+ local_time = timezone.localtime(lyngsat.last_update)
+ last_update_str = local_time.strftime("%d.%m.%Y")
+
+ data = {
+ 'id': lyngsat.id,
+ 'satellite': lyngsat.id_satellite.name if lyngsat.id_satellite else '-',
+ 'frequency': f"{lyngsat.frequency:.3f}" if lyngsat.frequency else '-',
+ 'polarization': lyngsat.polarization.name if lyngsat.polarization else '-',
+ 'modulation': lyngsat.modulation.name if lyngsat.modulation else '-',
+ 'standard': lyngsat.standard.name if lyngsat.standard else '-',
+ 'sym_velocity': f"{lyngsat.sym_velocity:.0f}" if lyngsat.sym_velocity else '-',
+ 'fec': lyngsat.fec or '-',
+ 'channel_info': lyngsat.channel_info or '-',
+ 'last_update': last_update_str,
+ 'url': lyngsat.url or None,
+ }
+
+ return JsonResponse(data)
+ except LyngSat.DoesNotExist:
+ return JsonResponse({'error': 'Источник LyngSat не найден'}, status=404)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
+
+
+class SigmaParameterDataAPIView(LoginRequiredMixin, View):
+ """API endpoint for getting SigmaParameter data."""
+
+ def get(self, request, parameter_id):
+ from ..models import Parameter
+
+ try:
+ parameter = Parameter.objects.select_related(
+ 'id_satellite',
+ 'polarization',
+ 'modulation',
+ 'standard'
+ ).prefetch_related(
+ 'sigma_parameter__mark',
+ 'sigma_parameter__id_satellite',
+ 'sigma_parameter__polarization',
+ 'sigma_parameter__modulation',
+ 'sigma_parameter__standard'
+ ).get(id=parameter_id)
+
+ # Get all related SigmaParameter
+ sigma_params = parameter.sigma_parameter.all()
+
+ sigma_data = []
+ for sigma in sigma_params:
+ # Get marks
+ marks = []
+ for mark in sigma.mark.all().order_by('-timestamp'):
+ mark_str = '+' if mark.mark else '-'
+ date_str = '-'
+ if mark.timestamp:
+ local_time = timezone.localtime(mark.timestamp)
+ date_str = local_time.strftime("%d.%m.%Y %H:%M")
+ marks.append({
+ 'mark': mark_str,
+ 'date': date_str
+ })
+
+ # Format start and end dates
+ datetime_begin_str = '-'
+ if sigma.datetime_begin:
+ local_time = timezone.localtime(sigma.datetime_begin)
+ datetime_begin_str = local_time.strftime("%d.%m.%Y %H:%M")
+
+ datetime_end_str = '-'
+ if sigma.datetime_end:
+ local_time = timezone.localtime(sigma.datetime_end)
+ datetime_end_str = local_time.strftime("%d.%m.%Y %H:%M")
+
+ sigma_data.append({
+ 'id': sigma.id,
+ 'satellite': sigma.id_satellite.name if sigma.id_satellite else '-',
+ 'frequency': f"{sigma.frequency:.3f}" if sigma.frequency else '-',
+ 'transfer_frequency': f"{sigma.transfer_frequency:.3f}" if sigma.transfer_frequency else '-',
+ 'freq_range': f"{sigma.freq_range:.3f}" if sigma.freq_range else '-',
+ 'polarization': sigma.polarization.name if sigma.polarization else '-',
+ 'modulation': sigma.modulation.name if sigma.modulation else '-',
+ 'standard': sigma.standard.name if sigma.standard else '-',
+ 'bod_velocity': f"{sigma.bod_velocity:.0f}" if sigma.bod_velocity else '-',
+ 'snr': f"{sigma.snr:.1f}" if sigma.snr is not None else '-',
+ 'power': f"{sigma.power:.1f}" if sigma.power is not None else '-',
+ 'status': sigma.status or '-',
+ 'packets': 'Да' if sigma.packets else 'Нет' if sigma.packets is not None else '-',
+ 'datetime_begin': datetime_begin_str,
+ 'datetime_end': datetime_end_str,
+ 'marks': marks
+ })
+
+ return JsonResponse({
+ 'parameter_id': parameter.id,
+ 'sigma_parameters': sigma_data
+ })
+ except Parameter.DoesNotExist:
+ return JsonResponse({'error': 'Parameter не найден'}, status=404)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
+
+
+class SourceObjItemsAPIView(LoginRequiredMixin, View):
+ """API endpoint for getting ObjItems related to a Source."""
+
+ def get(self, request, source_id):
+ from ..models import Source
+
+ try:
+ # Load Source with prefetch_related for 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)
+
+ # Get all related ObjItems, sorted by created_at
+ objitems = source.source_objitems.all().order_by('created_at')
+
+ objitems_data = []
+ for objitem in objitems:
+ # Get parameter data
+ 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 '-'
+
+ # Get geo data
+ 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 LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
+ """API endpoint for getting Celery task status."""
+
+ def get(self, request, task_id):
+ from celery.result import AsyncResult
+ from django.core.cache import cache
+
+ task = AsyncResult(task_id)
+
+ response_data = {
+ 'task_id': task_id,
+ 'state': task.state,
+ 'result': None,
+ 'error': None
+ }
+
+ if task.state == 'PENDING':
+ response_data['status'] = 'Задача в очереди...'
+ elif task.state == 'PROGRESS':
+ response_data['status'] = task.info.get('status', '')
+ response_data['current'] = task.info.get('current', 0)
+ response_data['total'] = task.info.get('total', 1)
+ response_data['percent'] = int((task.info.get('current', 0) / task.info.get('total', 1)) * 100)
+ elif task.state == 'SUCCESS':
+ # Get result from cache
+ result = cache.get(f'lyngsat_task_{task_id}')
+ if result:
+ response_data['result'] = result
+ response_data['status'] = 'Задача завершена успешно'
+ else:
+ response_data['result'] = task.result
+ response_data['status'] = 'Задача завершена'
+ elif task.state == 'FAILURE':
+ response_data['status'] = 'Ошибка при выполнении задачи'
+ response_data['error'] = str(task.info)
+ else:
+ response_data['status'] = task.state
+
+ return JsonResponse(response_data)
diff --git a/dbapp/mainapp/views/base.py b/dbapp/mainapp/views/base.py
new file mode 100644
index 0000000..024887b
--- /dev/null
+++ b/dbapp/mainapp/views/base.py
@@ -0,0 +1,22 @@
+"""
+Base views and utilities.
+"""
+from django.contrib.auth import logout
+from django.shortcuts import redirect, render
+from django.views import View
+
+
+class ActionsPageView(View):
+ """View for displaying the actions page."""
+
+ def get(self, request):
+ if request.user.is_authenticated:
+ return render(request, "mainapp/actions.html")
+ else:
+ return render(request, "mainapp/login_required.html")
+
+
+def custom_logout(request):
+ """Custom logout view."""
+ logout(request)
+ return redirect("mainapp:home")
diff --git a/dbapp/mainapp/views/data_import.py b/dbapp/mainapp/views/data_import.py
new file mode 100644
index 0000000..7c10782
--- /dev/null
+++ b/dbapp/mainapp/views/data_import.py
@@ -0,0 +1,205 @@
+"""
+Data import views (Excel, CSV, Transponders, VCH, etc.).
+"""
+from io import BytesIO
+
+from django.contrib import messages
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from django.urls import reverse_lazy
+from django.views import View
+from django.views.generic import FormView
+
+import pandas as pd
+
+from ..forms import (
+ LoadCsvData,
+ LoadExcelData,
+ NewEventForm,
+ UploadFileForm,
+ UploadVchLoad,
+ VchLinkForm,
+)
+from ..mixins import FormMessageMixin
+from ..utils import (
+ add_satellite_list,
+ compare_and_link_vch_load,
+ fill_data_from_df,
+ get_points_from_csv,
+ get_vch_load_from_html,
+ kub_report,
+)
+from mapsapp.utils import parse_transponders_from_xml
+
+
+class AddSatellitesView(LoginRequiredMixin, View):
+ """View for adding satellites to the database."""
+
+ def get(self, request):
+ add_satellite_list()
+ return redirect("mainapp:home")
+
+
+class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
+ """View for uploading and parsing transponder data from XML."""
+
+ template_name = "mainapp/transponders_upload.html"
+ form_class = UploadFileForm
+ success_message = "Файл успешно обработан"
+ error_message = "Форма заполнена некорректно"
+
+ def form_valid(self, form):
+ uploaded_file = self.request.FILES["file"]
+ try:
+ content = uploaded_file.read()
+ parse_transponders_from_xml(BytesIO(content))
+ except ValueError as e:
+ messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
+ return redirect("mainapp:add_trans")
+ except Exception as e:
+ messages.error(self.request, f"Неизвестная ошибка: {e}")
+ return redirect("mainapp:add_trans")
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return reverse_lazy("mainapp:add_trans")
+
+
+class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
+ """View for loading data from Excel files."""
+
+ template_name = "mainapp/add_data_from_excel.html"
+ form_class = LoadExcelData
+ error_message = "Форма заполнена некорректно"
+
+ def form_valid(self, form):
+ uploaded_file = self.request.FILES["file"]
+ selected_sat = form.cleaned_data["sat_choice"]
+ number = form.cleaned_data["number_input"]
+
+ try:
+ import io
+
+ df = pd.read_excel(io.BytesIO(uploaded_file.read()))
+ if number > 0:
+ df = df.head(number)
+ result = fill_data_from_df(df, selected_sat, self.request.user.customuser)
+
+ messages.success(
+ self.request, f"Данные успешно загружены! Обработано строк: {result}"
+ )
+ except Exception as e:
+ messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
+
+ return redirect("mainapp:load_excel_data")
+
+ def get_success_url(self):
+ return reverse_lazy("mainapp:load_excel_data")
+
+
+class LoadCsvDataView(LoginRequiredMixin, FormMessageMixin, FormView):
+ """View for loading data from CSV files."""
+
+ template_name = "mainapp/add_data_from_csv.html"
+ form_class = LoadCsvData
+ success_message = "Данные успешно загружены!"
+ error_message = "Форма заполнена некорректно"
+
+ def form_valid(self, form):
+ uploaded_file = self.request.FILES["file"]
+ try:
+ content = uploaded_file.read()
+ if isinstance(content, bytes):
+ content = content.decode("utf-8")
+
+ get_points_from_csv(content, self.request.user.customuser)
+ except Exception as e:
+ messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
+ return redirect("mainapp:load_csv_data")
+
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return reverse_lazy("mainapp:load_csv_data")
+
+
+class UploadVchLoadView(LoginRequiredMixin, FormMessageMixin, FormView):
+ """View for uploading VCH load data from HTML files."""
+
+ template_name = "mainapp/upload_html.html"
+ form_class = UploadVchLoad
+ success_message = "Файл успешно обработан"
+ error_message = "Форма заполнена некорректно"
+
+ def form_valid(self, form):
+ selected_sat = form.cleaned_data["sat_choice"]
+ uploaded_file = self.request.FILES["file"]
+ try:
+ get_vch_load_from_html(uploaded_file, selected_sat)
+ except ValueError as e:
+ messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
+ return redirect("mainapp:vch_load")
+ except Exception as e:
+ messages.error(self.request, f"Неизвестная ошибка: {e}")
+ return redirect("mainapp:vch_load")
+
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return reverse_lazy("mainapp:vch_load")
+
+
+class LinkVchSigmaView(LoginRequiredMixin, FormView):
+ """View for linking VCH data with Sigma parameters."""
+
+ template_name = "mainapp/link_vch.html"
+ form_class = VchLinkForm
+
+ def form_valid(self, form):
+ # value1 is no longer used - frequency tolerance is determined automatically
+ freq_range = form.cleaned_data["value2"]
+ sat_id = form.cleaned_data["sat_choice"]
+
+ # Pass 0 for eps_freq and ku_range as they are not used
+ count_all, link_count = compare_and_link_vch_load(sat_id, 0, freq_range, 0)
+
+ messages.success(
+ self.request, f"Привязано {link_count} из {count_all} объектов"
+ )
+ return redirect("mainapp:link_vch_sigma")
+
+ def form_invalid(self, form):
+ return self.render_to_response(self.get_context_data(form=form))
+
+
+class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
+ """View for processing Kubsat event data."""
+
+ template_name = "mainapp/process_kubsat.html"
+ form_class = NewEventForm
+ error_message = "Форма заполнена некорректно"
+
+ def form_valid(self, form):
+ uploaded_file = self.request.FILES["file"]
+ try:
+ content = uploaded_file.read()
+ df = kub_report(BytesIO(content))
+ output = BytesIO()
+ with pd.ExcelWriter(output, engine="openpyxl") as writer:
+ df.to_excel(writer, index=False, sheet_name="Результат")
+ output.seek(0)
+
+ response = HttpResponse(
+ output.getvalue(),
+ content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ )
+ response["Content-Disposition"] = (
+ 'attachment; filename="kubsat_report.xlsx"'
+ )
+
+ messages.success(self.request, "Событие успешно обработано!")
+ return response
+ except Exception as e:
+ messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
+ return redirect("mainapp:kubsat_excel")
diff --git a/dbapp/mainapp/views/lyngsat.py b/dbapp/mainapp/views/lyngsat.py
new file mode 100644
index 0000000..938a63a
--- /dev/null
+++ b/dbapp/mainapp/views/lyngsat.py
@@ -0,0 +1,161 @@
+"""
+LyngSat related views for data synchronization and linking.
+"""
+from django.contrib import messages
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.shortcuts import redirect, render
+from django.urls import reverse_lazy
+from django.views import View
+from django.views.generic import FormView
+
+from ..forms import FillLyngsatDataForm, LinkLyngsatForm
+from ..mixins import FormMessageMixin
+from ..models import ObjItem
+
+
+class LinkLyngsatSourcesView(LoginRequiredMixin, FormMessageMixin, FormView):
+ """View for linking LyngSat sources to objects."""
+
+ template_name = "mainapp/link_lyngsat.html"
+ form_class = LinkLyngsatForm
+ success_message = "Привязка источников LyngSat завершена"
+ error_message = "Ошибка при привязке источников"
+
+ def form_valid(self, form):
+ from lyngsatapp.models import LyngSat
+
+ satellites = form.cleaned_data.get("satellites")
+ frequency_tolerance = form.cleaned_data.get("frequency_tolerance", 0.5)
+
+ # If satellites not selected, process all
+ if satellites:
+ objitems = ObjItem.objects.filter(
+ parameter_obj__id_satellite__in=satellites
+ ).select_related('parameter_obj', 'parameter_obj__polarization')
+ else:
+ objitems = ObjItem.objects.filter(
+ parameter_obj__isnull=False
+ ).select_related('parameter_obj', 'parameter_obj__polarization')
+
+ linked_count = 0
+ total_count = objitems.count()
+
+ for objitem in objitems:
+ if not hasattr(objitem, 'parameter_obj') or not objitem.parameter_obj:
+ continue
+
+ param = objitem.parameter_obj
+
+ # Round object frequency
+ if param.frequency:
+ rounded_freq = round(param.frequency, 0) # Round to integer
+
+ # Find matching LyngSat source
+ # Compare by rounded frequency and polarization
+ lyngsat_sources = LyngSat.objects.filter(
+ id_satellite=param.id_satellite,
+ polarization=param.polarization,
+ frequency__gte=rounded_freq - frequency_tolerance,
+ frequency__lte=rounded_freq + frequency_tolerance
+ ).order_by('frequency')
+
+ if lyngsat_sources.exists():
+ # Take first matching source
+ objitem.lyngsat_source = lyngsat_sources.first()
+ objitem.save(update_fields=['lyngsat_source'])
+ linked_count += 1
+
+ messages.success(
+ self.request,
+ f"Привязано {linked_count} из {total_count} объектов к источникам LyngSat"
+ )
+ return redirect("mainapp:link_lyngsat")
+
+ def form_invalid(self, form):
+ return self.render_to_response(self.get_context_data(form=form))
+
+
+class FillLyngsatDataView(LoginRequiredMixin, FormMessageMixin, FormView):
+ """
+ View for filling data from Lyngsat.
+
+ Allows selecting satellites and regions for parsing data from Lyngsat website.
+ Starts asynchronous Celery task for processing.
+ """
+ template_name = "mainapp/fill_lyngsat_data.html"
+ form_class = FillLyngsatDataForm
+ success_url = reverse_lazy("mainapp:lyngsat_task_status")
+ error_message = "Форма заполнена некорректно"
+
+ def form_valid(self, form):
+ satellites = form.cleaned_data["satellites"]
+ regions = form.cleaned_data["regions"]
+ use_cache = form.cleaned_data.get("use_cache", True)
+ force_refresh = form.cleaned_data.get("force_refresh", False)
+
+ # Get satellite names
+ target_sats = [sat.name for sat in satellites]
+
+ try:
+ from lyngsatapp.tasks import fill_lyngsat_data_task
+
+ # Start asynchronous task with caching parameters
+ task = fill_lyngsat_data_task.delay(
+ target_sats,
+ regions,
+ force_refresh=force_refresh,
+ use_cache=use_cache
+ )
+
+ cache_status = "без кеша" if not use_cache else ("с обновлением кеша" if force_refresh else "с кешированием")
+
+ messages.success(
+ self.request,
+ f"Задача запущена ({cache_status})! ID задачи: {task.id}. "
+ "Вы будете перенаправлены на страницу отслеживания прогресса."
+ )
+
+ # Redirect to task status page
+ return redirect('mainapp:lyngsat_task_status', task_id=task.id)
+
+ except Exception as e:
+ messages.error(self.request, f"Ошибка при запуске задачи: {str(e)}")
+ return redirect("mainapp:fill_lyngsat_data")
+
+
+class LyngsatTaskStatusView(LoginRequiredMixin, View):
+ """View for tracking Lyngsat data filling task status."""
+
+ template_name = "mainapp/lyngsat_task_status.html"
+
+ def get(self, request, task_id=None):
+ context = {
+ 'task_id': task_id
+ }
+ return render(request, self.template_name, context)
+
+
+class ClearLyngsatCacheView(LoginRequiredMixin, View):
+ """View for clearing LyngSat cache."""
+
+ def post(self, request):
+ from lyngsatapp.tasks import clear_cache_task
+
+ cache_type = request.POST.get('cache_type', 'all')
+
+ try:
+ # Start cache clearing task
+ task = clear_cache_task.delay(cache_type)
+
+ messages.success(
+ request,
+ f"Задача очистки кеша ({cache_type}) запущена! ID задачи: {task.id}"
+ )
+ except Exception as e:
+ messages.error(request, f"Ошибка при запуске задачи очистки кеша: {str(e)}")
+
+ return redirect(request.META.get('HTTP_REFERER', 'mainapp:home'))
+
+ def get(self, request):
+ """Cache management page."""
+ return render(request, 'mainapp/clear_lyngsat_cache.html')
diff --git a/dbapp/mainapp/views/map.py b/dbapp/mainapp/views/map.py
new file mode 100644
index 0000000..59f2577
--- /dev/null
+++ b/dbapp/mainapp/views/map.py
@@ -0,0 +1,139 @@
+"""
+Map related views for displaying objects on maps.
+"""
+from collections import defaultdict
+
+from django.contrib.admin.views.decorators import staff_member_required
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import JsonResponse
+from django.shortcuts import redirect, render
+from django.utils.decorators import method_decorator
+from django.views import View
+
+from ..clusters import get_clusters
+from ..mixins import RoleRequiredMixin
+from ..models import ObjItem
+
+
+@method_decorator(staff_member_required, name="dispatch")
+class ShowMapView(RoleRequiredMixin, View):
+ """View for displaying objects on map (admin interface)."""
+
+ required_roles = ["admin", "moderator"]
+
+ def get(self, request):
+ ids = request.GET.get("ids", "")
+ points = []
+ if ids:
+ id_list = [int(x) for x in ids.split(",") if x.isdigit()]
+ locations = ObjItem.objects.filter(id__in=id_list).select_related(
+ "parameter_obj",
+ "parameter_obj__id_satellite",
+ "parameter_obj__polarization",
+ "parameter_obj__modulation",
+ "parameter_obj__standard",
+ "geo_obj",
+ )
+ for obj in locations:
+ if (
+ not hasattr(obj, "geo_obj")
+ or not obj.geo_obj
+ or not obj.geo_obj.coords
+ ):
+ continue
+ param = getattr(obj, 'parameter_obj', None)
+ if not param:
+ continue
+ points.append(
+ {
+ "name": f"{obj.name}",
+ "freq": f"{param.frequency} [{param.freq_range}] МГц",
+ "point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
+ }
+ )
+ else:
+ return redirect("admin")
+
+ grouped = defaultdict(list)
+ for p in points:
+ grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
+
+ groups = [
+ {"name": name, "points": coords_list}
+ for name, coords_list in grouped.items()
+ ]
+
+ context = {
+ "groups": groups,
+ }
+ return render(request, "admin/map_custom.html", context)
+
+
+class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
+ """View for displaying selected objects on map."""
+
+ def get(self, request):
+ ids = request.GET.get("ids", "")
+ points = []
+ if ids:
+ id_list = [int(x) for x in ids.split(",") if x.isdigit()]
+ locations = ObjItem.objects.filter(id__in=id_list).select_related(
+ "parameter_obj",
+ "parameter_obj__id_satellite",
+ "parameter_obj__polarization",
+ "parameter_obj__modulation",
+ "parameter_obj__standard",
+ "geo_obj",
+ )
+ for obj in locations:
+ if (
+ not hasattr(obj, "geo_obj")
+ or not obj.geo_obj
+ or not obj.geo_obj.coords
+ ):
+ continue
+ param = getattr(obj, 'parameter_obj', None)
+ if not param:
+ continue
+ points.append(
+ {
+ "name": f"{obj.name}",
+ "freq": f"{param.frequency} [{param.freq_range}] МГц",
+ "point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
+ }
+ )
+ else:
+ return redirect("mainapp:objitem_list")
+
+ # Group points by object name
+ grouped = defaultdict(list)
+ for p in points:
+ grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
+
+ groups = [
+ {"name": name, "points": coords_list}
+ for name, coords_list in grouped.items()
+ ]
+
+ context = {
+ "groups": groups,
+ }
+ return render(request, "mainapp/objitem_map.html", context)
+
+
+class ClusterTestView(LoginRequiredMixin, View):
+ """Test view for clustering functionality."""
+
+ def get(self, request):
+ objs = ObjItem.objects.filter(
+ name__icontains="! Astra 4A 12654,040 [1,962] МГц H"
+ )
+ coords = []
+ for obj in objs:
+ if hasattr(obj, "geo_obj") and obj.geo_obj and obj.geo_obj.coords:
+ coords.append(
+ (obj.geo_obj.coords.coords[1], obj.geo_obj.coords.coords[0])
+ )
+ get_clusters(coords)
+
+ return JsonResponse({"success": "ок"})
diff --git a/dbapp/mainapp/views/objitem.py b/dbapp/mainapp/views/objitem.py
new file mode 100644
index 0000000..d726855
--- /dev/null
+++ b/dbapp/mainapp/views/objitem.py
@@ -0,0 +1,666 @@
+"""
+ObjItem CRUD operations and related views.
+"""
+from django.contrib import messages
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.core.paginator import Paginator
+from django.db import models
+from django.db.models import F
+from django.http import JsonResponse
+from django.shortcuts import redirect, render
+from django.urls import reverse_lazy
+from django.views import View
+from django.views.generic import CreateView, DeleteView, UpdateView
+
+from ..forms import GeoForm, ObjItemForm, ParameterForm
+from ..mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
+from ..models import Geo, Modulation, ObjItem, Polarization, Satellite
+from ..utils import parse_pagination_params
+
+
+class DeleteSelectedObjectsView(RoleRequiredMixin, View):
+ """View for deleting multiple selected objects."""
+
+ required_roles = ["admin", "moderator"]
+
+ def post(self, request):
+ ids = request.POST.get("ids", "")
+ if not ids:
+ return JsonResponse({"error": "Нет ID для удаления"}, status=400)
+
+ try:
+ id_list = [int(x) for x in ids.split(",") if x.isdigit()]
+ deleted_count, _ = ObjItem.objects.filter(id__in=id_list).delete()
+
+ return JsonResponse(
+ {
+ "success": True,
+ "message": "Объект успешно удалён",
+ "deleted_count": deleted_count,
+ }
+ )
+ except Exception as e:
+ return JsonResponse({"error": f"Ошибка при удалении: {str(e)}"}, status=500)
+
+
+class ObjItemListView(LoginRequiredMixin, View):
+ """View for displaying a list of ObjItems with filtering and pagination."""
+
+ def get(self, request):
+ satellites = (
+ Satellite.objects.filter(parameters__objitem__isnull=False)
+ .distinct()
+ .only("id", "name")
+ .order_by("name")
+ )
+
+ selected_sat_id = request.GET.get("satellite_id")
+ page_number, items_per_page = parse_pagination_params(request)
+ sort_param = request.GET.get("sort", "")
+
+ freq_min = request.GET.get("freq_min")
+ freq_max = request.GET.get("freq_max")
+ range_min = request.GET.get("range_min")
+ range_max = request.GET.get("range_max")
+ snr_min = request.GET.get("snr_min")
+ snr_max = request.GET.get("snr_max")
+ bod_min = request.GET.get("bod_min")
+ bod_max = request.GET.get("bod_max")
+ search_query = request.GET.get("search")
+ selected_modulations = request.GET.getlist("modulation")
+ selected_polarizations = request.GET.getlist("polarization")
+ selected_satellites = request.GET.getlist("satellite_id")
+ has_kupsat = request.GET.get("has_kupsat")
+ has_valid = request.GET.get("has_valid")
+ date_from = request.GET.get("date_from")
+ date_to = request.GET.get("date_to")
+
+ objects = ObjItem.objects.none()
+
+ if selected_satellites or selected_sat_id:
+ if selected_sat_id and not selected_satellites:
+ try:
+ selected_sat_id_single = int(selected_sat_id)
+ selected_satellites = [selected_sat_id_single]
+ except ValueError:
+ selected_satellites = []
+
+ if selected_satellites:
+ objects = (
+ ObjItem.objects.select_related(
+ "geo_obj",
+ "source",
+ "updated_by__user",
+ "created_by__user",
+ "lyngsat_source",
+ "parameter_obj",
+ "parameter_obj__id_satellite",
+ "parameter_obj__polarization",
+ "parameter_obj__modulation",
+ "parameter_obj__standard",
+ )
+ .prefetch_related(
+ "parameter_obj__sigma_parameter",
+ "parameter_obj__sigma_parameter__polarization",
+ )
+ .filter(parameter_obj__id_satellite_id__in=selected_satellites)
+ )
+ else:
+ objects = ObjItem.objects.select_related(
+ "geo_obj",
+ "source",
+ "updated_by__user",
+ "created_by__user",
+ "lyngsat_source",
+ "parameter_obj",
+ "parameter_obj__id_satellite",
+ "parameter_obj__polarization",
+ "parameter_obj__modulation",
+ "parameter_obj__standard",
+ ).prefetch_related(
+ "parameter_obj__sigma_parameter",
+ "parameter_obj__sigma_parameter__polarization",
+ )
+
+ if freq_min is not None and freq_min.strip() != "":
+ try:
+ freq_min_val = float(freq_min)
+ objects = objects.filter(
+ parameter_obj__frequency__gte=freq_min_val
+ )
+ except ValueError:
+ pass
+ if freq_max is not None and freq_max.strip() != "":
+ try:
+ freq_max_val = float(freq_max)
+ objects = objects.filter(
+ parameter_obj__frequency__lte=freq_max_val
+ )
+ except ValueError:
+ pass
+
+ if range_min is not None and range_min.strip() != "":
+ try:
+ range_min_val = float(range_min)
+ objects = objects.filter(
+ parameter_obj__freq_range__gte=range_min_val
+ )
+ except ValueError:
+ pass
+ if range_max is not None and range_max.strip() != "":
+ try:
+ range_max_val = float(range_max)
+ objects = objects.filter(
+ parameter_obj__freq_range__lte=range_max_val
+ )
+ except ValueError:
+ pass
+
+ if snr_min is not None and snr_min.strip() != "":
+ try:
+ snr_min_val = float(snr_min)
+ objects = objects.filter(parameter_obj__snr__gte=snr_min_val)
+ except ValueError:
+ pass
+ if snr_max is not None and snr_max.strip() != "":
+ try:
+ snr_max_val = float(snr_max)
+ objects = objects.filter(parameter_obj__snr__lte=snr_max_val)
+ except ValueError:
+ pass
+
+ if bod_min is not None and bod_min.strip() != "":
+ try:
+ bod_min_val = float(bod_min)
+ objects = objects.filter(
+ parameter_obj__bod_velocity__gte=bod_min_val
+ )
+ except ValueError:
+ pass
+ if bod_max is not None and bod_max.strip() != "":
+ try:
+ bod_max_val = float(bod_max)
+ objects = objects.filter(
+ parameter_obj__bod_velocity__lte=bod_max_val
+ )
+ except ValueError:
+ pass
+
+ if selected_modulations:
+ objects = objects.filter(
+ parameter_obj__modulation__id__in=selected_modulations
+ )
+
+ if selected_polarizations:
+ objects = objects.filter(
+ parameter_obj__polarization__id__in=selected_polarizations
+ )
+
+ if has_kupsat == "1":
+ objects = objects.filter(source__coords_kupsat__isnull=False)
+ elif has_kupsat == "0":
+ objects = objects.filter(source__coords_kupsat__isnull=True)
+
+ if has_valid == "1":
+ objects = objects.filter(source__coords_valid__isnull=False)
+ elif has_valid == "0":
+ objects = objects.filter(source__coords_valid__isnull=True)
+
+ # Date filter for geo_obj timestamp
+ if date_from and date_from.strip():
+ try:
+ from datetime import datetime
+ date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
+ objects = objects.filter(geo_obj__timestamp__gte=date_from_obj)
+ except (ValueError, TypeError):
+ pass
+
+ if date_to and date_to.strip():
+ try:
+ from datetime import datetime, timedelta
+ date_to_obj = datetime.strptime(date_to, "%Y-%m-%d")
+ # Add one day to include the entire end date
+ date_to_obj = date_to_obj + timedelta(days=1)
+ objects = objects.filter(geo_obj__timestamp__lt=date_to_obj)
+ except (ValueError, TypeError):
+ pass
+
+ # Filter by source type (lyngsat_source)
+ has_source_type = request.GET.get("has_source_type")
+ if has_source_type == "1":
+ objects = objects.filter(lyngsat_source__isnull=False)
+ elif has_source_type == "0":
+ objects = objects.filter(lyngsat_source__isnull=True)
+
+ # Filter by sigma (sigma parameters)
+ has_sigma = request.GET.get("has_sigma")
+ if has_sigma == "1":
+ objects = objects.filter(parameter_obj__sigma_parameter__isnull=False)
+ elif has_sigma == "0":
+ objects = objects.filter(parameter_obj__sigma_parameter__isnull=True)
+
+ if search_query:
+ search_query = search_query.strip()
+ if search_query:
+ objects = objects.filter(
+ models.Q(name__icontains=search_query)
+ | models.Q(geo_obj__location__icontains=search_query)
+ )
+ else:
+ selected_sat_id = None
+
+ objects = objects.annotate(
+ first_param_freq=F("parameter_obj__frequency"),
+ first_param_range=F("parameter_obj__freq_range"),
+ first_param_snr=F("parameter_obj__snr"),
+ first_param_bod=F("parameter_obj__bod_velocity"),
+ first_param_sat_name=F("parameter_obj__id_satellite__name"),
+ first_param_pol_name=F("parameter_obj__polarization__name"),
+ first_param_mod_name=F("parameter_obj__modulation__name"),
+ )
+
+ valid_sort_fields = {
+ "name": "name",
+ "-name": "-name",
+ "updated_at": "updated_at",
+ "-updated_at": "-updated_at",
+ "created_at": "created_at",
+ "-created_at": "-created_at",
+ "updated_by": "updated_by__user__username",
+ "-updated_by": "-updated_by__user__username",
+ "created_by": "created_by__user__username",
+ "-created_by": "-created_by__user__username",
+ "geo_timestamp": "geo_obj__timestamp",
+ "-geo_timestamp": "-geo_obj__timestamp",
+ "frequency": "first_param_freq",
+ "-frequency": "-first_param_freq",
+ "freq_range": "first_param_range",
+ "-freq_range": "-first_param_range",
+ "snr": "first_param_snr",
+ "-snr": "-first_param_snr",
+ "bod_velocity": "first_param_bod",
+ "-bod_velocity": "-first_param_bod",
+ "satellite": "first_param_sat_name",
+ "-satellite": "-first_param_sat_name",
+ "polarization": "first_param_pol_name",
+ "-polarization": "-first_param_pol_name",
+ "modulation": "first_param_mod_name",
+ "-modulation": "-first_param_mod_name",
+ }
+
+ if sort_param in valid_sort_fields:
+ objects = objects.order_by(valid_sort_fields[sort_param])
+
+ paginator = Paginator(objects, items_per_page)
+ page_obj = paginator.get_page(page_number)
+
+ processed_objects = []
+ for obj in page_obj:
+ param = getattr(obj, 'parameter_obj', None)
+
+ geo_coords = "-"
+ geo_timestamp = "-"
+ geo_location = "-"
+ kupsat_coords = "-"
+ valid_coords = "-"
+ distance_geo_kup = "-"
+ distance_geo_valid = "-"
+ distance_kup_valid = "-"
+
+ if hasattr(obj, "geo_obj") and obj.geo_obj:
+ geo_timestamp = obj.geo_obj.timestamp
+ geo_location = obj.geo_obj.location
+
+ if obj.geo_obj.coords:
+ longitude = obj.geo_obj.coords.coords[0]
+ latitude = obj.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}"
+
+ satellite_name = "-"
+ frequency = "-"
+ freq_range = "-"
+ polarization_name = "-"
+ bod_velocity = "-"
+ modulation_name = "-"
+ snr = "-"
+ standard_name = "-"
+ comment = "-"
+ is_average = "-"
+
+ if param:
+ if hasattr(param, "id_satellite") and param.id_satellite:
+ satellite_name = (
+ param.id_satellite.name
+ if hasattr(param.id_satellite, "name")
+ else "-"
+ )
+
+ 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 "-"
+ )
+ bod_velocity = (
+ f"{param.bod_velocity:.0f}"
+ if param.bod_velocity is not None
+ else "-"
+ )
+ snr = f"{param.snr:.0f}" if param.snr is not None else "-"
+
+ if hasattr(param, "polarization") and param.polarization:
+ polarization_name = (
+ param.polarization.name
+ if hasattr(param.polarization, "name")
+ else "-"
+ )
+
+ if hasattr(param, "modulation") and param.modulation:
+ modulation_name = (
+ param.modulation.name
+ if hasattr(param.modulation, "name")
+ else "-"
+ )
+
+ if hasattr(param, "standard") and param.standard:
+ standard_name = (
+ param.standard.name
+ if hasattr(param.standard, "name")
+ else "-"
+ )
+
+ if hasattr(obj, "geo_obj") and obj.geo_obj:
+ comment = obj.geo_obj.comment or "-"
+ is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-"
+
+ source_type = "ТВ" if obj.lyngsat_source else "-"
+
+ has_sigma = False
+ sigma_info = "-"
+ if param:
+ sigma_count = param.sigma_parameter.count()
+ if sigma_count > 0:
+ has_sigma = True
+ first_sigma = param.sigma_parameter.first()
+ if first_sigma:
+ sigma_freq = f"{first_sigma.transfer_frequency:.3f}" if first_sigma.transfer_frequency else "-"
+ sigma_range = f"{first_sigma.freq_range:.3f}" if first_sigma.freq_range else "-"
+ sigma_pol = first_sigma.polarization.name if first_sigma.polarization else "-"
+ sigma_pol_short = sigma_pol[0] if sigma_pol and sigma_pol != "-" else "-"
+ sigma_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}"
+
+ processed_objects.append(
+ {
+ "id": obj.id,
+ "name": obj.name or "-",
+ "satellite_name": satellite_name,
+ "frequency": frequency,
+ "freq_range": freq_range,
+ "polarization": polarization_name,
+ "bod_velocity": bod_velocity,
+ "modulation": modulation_name,
+ "snr": snr,
+ "geo_timestamp": geo_timestamp,
+ "geo_location": geo_location,
+ "geo_coords": geo_coords,
+ "kupsat_coords": kupsat_coords,
+ "valid_coords": valid_coords,
+ "distance_geo_kup": distance_geo_kup,
+ "distance_geo_valid": distance_geo_valid,
+ "distance_kup_valid": distance_kup_valid,
+ "updated_by": obj.updated_by if obj.updated_by else "-",
+ "comment": comment,
+ "is_average": is_average,
+ "source_type": source_type,
+ "standard": standard_name,
+ "has_sigma": has_sigma,
+ "sigma_info": sigma_info,
+ "obj": obj,
+ }
+ )
+
+ modulations = Modulation.objects.all()
+ polarizations = Polarization.objects.all()
+
+ # Get the new filter values
+ has_source_type = request.GET.get("has_source_type")
+ has_sigma = request.GET.get("has_sigma")
+
+ context = {
+ "satellites": satellites,
+ "selected_satellite_id": selected_sat_id,
+ "page_obj": page_obj,
+ "processed_objects": processed_objects,
+ "items_per_page": items_per_page,
+ "available_items_per_page": [50, 100, 500, 1000],
+ "freq_min": freq_min,
+ "freq_max": freq_max,
+ "range_min": range_min,
+ "range_max": range_max,
+ "snr_min": snr_min,
+ "snr_max": snr_max,
+ "bod_min": bod_min,
+ "bod_max": bod_max,
+ "search_query": search_query,
+ "selected_modulations": [
+ int(x) for x in selected_modulations if x.isdigit()
+ ],
+ "selected_polarizations": [
+ int(x) for x in selected_polarizations if x.isdigit()
+ ],
+ "selected_satellites": [int(x) for x in selected_satellites if x.isdigit()],
+ "has_kupsat": has_kupsat,
+ "has_valid": has_valid,
+ "date_from": date_from,
+ "date_to": date_to,
+ "has_source_type": has_source_type,
+ "has_sigma": has_sigma,
+ "modulations": modulations,
+ "polarizations": polarizations,
+ "full_width_page": True,
+ "sort": sort_param,
+ }
+
+ return render(request, "mainapp/objitem_list.html", context)
+
+
+class ObjItemFormView(
+ RoleRequiredMixin, CoordinateProcessingMixin, FormMessageMixin, UpdateView
+):
+ """
+ Base class for creating and editing ObjItem.
+
+ Contains common logic for form processing, coordinates, and parameters.
+ """
+
+ model = ObjItem
+ form_class = ObjItemForm
+ template_name = "mainapp/objitem_form.html"
+ success_url = reverse_lazy("mainapp:home")
+ required_roles = ["admin", "moderator"]
+
+ def get_success_url(self):
+ """Returns URL with saved filter parameters."""
+ if self.request.GET:
+ from urllib.parse import urlencode
+ query_string = urlencode(self.request.GET)
+ return reverse_lazy("mainapp:objitem_list") + '?' + query_string
+ return reverse_lazy("mainapp:objitem_list")
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["LEAFLET_CONFIG"] = {
+ "DEFAULT_CENTER": (55.75, 37.62),
+ "DEFAULT_ZOOM": 5,
+ }
+
+ # Save return parameters for "Back" button
+ context["return_params"] = self.request.GET.get('return_params', '')
+
+ # Work with single parameter form instead of formset
+ if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
+ context["parameter_form"] = ParameterForm(
+ instance=self.object.parameter_obj, prefix="parameter"
+ )
+ else:
+ context["parameter_form"] = ParameterForm(prefix="parameter")
+
+ if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
+ context["geo_form"] = GeoForm(
+ instance=self.object.geo_obj, prefix="geo"
+ )
+ else:
+ context["geo_form"] = GeoForm(prefix="geo")
+
+ return context
+
+ def form_valid(self, form):
+ # Get parameter form
+ if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
+ parameter_form = ParameterForm(
+ self.request.POST,
+ instance=self.object.parameter_obj,
+ prefix="parameter"
+ )
+ else:
+ parameter_form = ParameterForm(self.request.POST, prefix="parameter")
+
+ if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
+ geo_form = GeoForm(self.request.POST, instance=self.object.geo_obj, prefix="geo")
+ else:
+ geo_form = GeoForm(self.request.POST, prefix="geo")
+
+ # Save main object
+ self.object = form.save(commit=False)
+ self.set_user_fields()
+ self.object.save()
+
+ # Save related parameter
+ if parameter_form.is_valid():
+ self.save_parameter(parameter_form)
+ else:
+ context = self.get_context_data()
+ context.update({
+ 'form': form,
+ 'parameter_form': parameter_form,
+ 'geo_form': geo_form,
+ })
+ return self.render_to_response(context)
+
+ # Save geo data
+ if geo_form.is_valid():
+ self.save_geo_data(geo_form)
+ else:
+ context = self.get_context_data()
+ context.update({
+ 'form': form,
+ 'parameter_form': parameter_form,
+ 'geo_form': geo_form,
+ })
+ return self.render_to_response(context)
+
+ return super().form_valid(form)
+
+ def set_user_fields(self):
+ """Sets user fields for the object."""
+ raise NotImplementedError("Subclasses must implement set_user_fields()")
+
+ def save_parameter(self, parameter_form):
+ """Saves object parameter through OneToOne relationship."""
+ if parameter_form.is_valid():
+ instance = parameter_form.save(commit=False)
+ instance.objitem = self.object
+ instance.save()
+
+ def save_geo_data(self, geo_form):
+ """Saves object geo data."""
+ geo_instance = self.get_or_create_geo_instance()
+
+ # Update fields from geo_form
+ if geo_form.is_valid():
+ geo_instance.location = geo_form.cleaned_data["location"]
+ geo_instance.comment = geo_form.cleaned_data["comment"]
+ geo_instance.is_average = geo_form.cleaned_data["is_average"]
+
+ # Process date/time
+ self.process_timestamp(geo_instance)
+
+ geo_instance.save()
+
+ def get_or_create_geo_instance(self):
+ """Gets or creates Geo instance."""
+ if hasattr(self.object, "geo_obj") and self.object.geo_obj:
+ return self.object.geo_obj
+ return Geo(objitem=self.object)
+
+
+class ObjItemUpdateView(ObjItemFormView):
+ """View for editing ObjItem."""
+
+ success_message = "Объект успешно сохранён!"
+
+ def set_user_fields(self):
+ self.object.updated_by = self.request.user.customuser
+
+
+class ObjItemCreateView(ObjItemFormView, CreateView):
+ """View for creating ObjItem."""
+
+ success_message = "Объект успешно создан!"
+
+ def set_user_fields(self):
+ self.object.created_by = self.request.user.customuser
+ self.object.updated_by = self.request.user.customuser
+
+
+class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
+ """View for deleting ObjItem."""
+
+ model = ObjItem
+ template_name = "mainapp/objitem_confirm_delete.html"
+ success_url = reverse_lazy("mainapp:objitem_list")
+ success_message = "Объект успешно удалён!"
+ required_roles = ["admin", "moderator"]
+
+ def get_success_url(self):
+ """Returns URL with saved filter parameters."""
+ if self.request.GET:
+ from urllib.parse import urlencode
+ query_string = urlencode(self.request.GET)
+ return reverse_lazy("mainapp:objitem_list") + '?' + query_string
+ return reverse_lazy("mainapp:objitem_list")
+
+
+class ObjItemDetailView(LoginRequiredMixin, View):
+ """
+ View for displaying ObjItem details in read-only mode.
+
+ Available to all authenticated users, displays data in read-only mode.
+ """
+ def get(self, request, pk):
+ obj = ObjItem.objects.filter(pk=pk).select_related(
+ 'geo_obj',
+ 'updated_by__user',
+ 'created_by__user',
+ 'parameter_obj',
+ 'parameter_obj__id_satellite',
+ 'parameter_obj__polarization',
+ 'parameter_obj__modulation',
+ 'parameter_obj__standard',
+ ).first()
+
+ if not obj:
+ from django.http import Http404
+ raise Http404("Объект не найден")
+
+ # Save return parameters for "Back" button
+ return_params = request.GET.get('return_params', '')
+
+ context = {
+ 'object': obj,
+ 'return_params': return_params
+ }
+
+ return render(request, "mainapp/objitem_detail.html", context)
diff --git a/dbapp/mainapp/views/source.py b/dbapp/mainapp/views/source.py
new file mode 100644
index 0000000..a0766f4
--- /dev/null
+++ b/dbapp/mainapp/views/source.py
@@ -0,0 +1,190 @@
+"""
+Source related views.
+"""
+from datetime import datetime
+
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.core.paginator import Paginator
+from django.db.models import Count
+from django.shortcuts import render
+from django.views import View
+
+from ..models import Source
+from ..utils import parse_pagination_params
+
+
+class SourceListView(LoginRequiredMixin, View):
+ """
+ View for displaying a list of sources (Source).
+ """
+
+ def get(self, request):
+ # Get pagination parameters
+ page_number, items_per_page = parse_pagination_params(request)
+
+ # Get sorting parameters
+ sort_param = request.GET.get("sort", "-created_at")
+
+ # Get filter parameters
+ 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()
+
+ # Get all Source objects with query optimization
+ 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')
+ )
+
+ # Apply filters
+ # Filter by coords_average presence
+ if has_coords_average == "1":
+ sources = sources.filter(coords_average__isnull=False)
+ elif has_coords_average == "0":
+ sources = sources.filter(coords_average__isnull=True)
+
+ # Filter by coords_kupsat presence
+ if has_coords_kupsat == "1":
+ sources = sources.filter(coords_kupsat__isnull=False)
+ elif has_coords_kupsat == "0":
+ sources = sources.filter(coords_kupsat__isnull=True)
+
+ # Filter by coords_valid presence
+ if has_coords_valid == "1":
+ sources = sources.filter(coords_valid__isnull=False)
+ elif has_coords_valid == "0":
+ sources = sources.filter(coords_valid__isnull=True)
+
+ # Filter by coords_reference presence
+ if has_coords_reference == "1":
+ sources = sources.filter(coords_reference__isnull=False)
+ elif has_coords_reference == "0":
+ sources = sources.filter(coords_reference__isnull=True)
+
+ # Filter by ObjItem count
+ 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
+
+ # Filter by creation date range
+ 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")
+ # Add one day to include entire end date
+ date_to_obj = date_to_obj + timedelta(days=1)
+ sources = sources.filter(created_at__lt=date_to_obj)
+ except (ValueError, TypeError):
+ pass
+
+ # Search by ID
+ if search_query:
+ try:
+ search_id = int(search_query)
+ sources = sources.filter(id=search_id)
+ except ValueError:
+ # If not a number, ignore
+ pass
+
+ # Apply sorting
+ 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])
+
+ # Create paginator
+ paginator = Paginator(sources, items_per_page)
+ page_obj = paginator.get_page(page_number)
+
+ # Prepare data for display
+ processed_sources = []
+ for source in page_obj:
+ # Format coordinates
+ 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)
+
+ # Get count of related ObjItems
+ 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,
+ })
+
+ # Prepare context for template
+ 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)
diff --git a/dbapp/mainapp/views.py b/dbapp/mainapp/views_old.py
similarity index 99%
rename from dbapp/mainapp/views.py
rename to dbapp/mainapp/views_old.py
index af8e5cb..c753f27 100644
--- a/dbapp/mainapp/views.py
+++ b/dbapp/mainapp/views_old.py
@@ -61,15 +61,6 @@ class AddSatellitesView(LoginRequiredMixin, View):
return redirect("mainapp:home")
-# class AddTranspondersView(View):
-# def get(self, request):
-# try:
-# parse_transponders_from_json(BASE_DIR / "transponders.json")
-# except FileNotFoundError:
-# print("Файл не найден")
-# return redirect('home')
-
-
class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
template_name = "mainapp/transponders_upload.html"
form_class = UploadFileForm
@@ -1414,9 +1405,6 @@ class ObjItemFormView(
geo_instance.comment = geo_form.cleaned_data["comment"]
geo_instance.is_average = geo_form.cleaned_data["is_average"]
- # Обрабатываем координаты
- self.process_coordinates(geo_instance)
-
# Обрабатываем дату/время
self.process_timestamp(geo_instance)