From 122fe74e140ac23031b74702ea7e4e5945b5cd04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Thu, 13 Nov 2025 16:11:37 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dbapp/dbapp/urls.py | 4 +- dbapp/mainapp/mixins.py | 438 ++++--- .../templates/mainapp/objitem_detail.html | 790 +++++-------- .../templates/mainapp/objitem_form.html | 1049 +++++++---------- dbapp/mainapp/urls.py | 88 +- dbapp/mainapp/views/README.md | 75 ++ dbapp/mainapp/views/__init__.py | 72 ++ dbapp/mainapp/views/api.py | 301 +++++ dbapp/mainapp/views/base.py | 22 + dbapp/mainapp/views/data_import.py | 205 ++++ dbapp/mainapp/views/lyngsat.py | 161 +++ dbapp/mainapp/views/map.py | 139 +++ dbapp/mainapp/views/objitem.py | 666 +++++++++++ dbapp/mainapp/views/source.py | 190 +++ dbapp/mainapp/{views.py => views_old.py} | 12 - 15 files changed, 2866 insertions(+), 1346 deletions(-) create mode 100644 dbapp/mainapp/views/README.md create mode 100644 dbapp/mainapp/views/__init__.py create mode 100644 dbapp/mainapp/views/api.py create mode 100644 dbapp/mainapp/views/base.py create mode 100644 dbapp/mainapp/views/data_import.py create mode 100644 dbapp/mainapp/views/lyngsat.py create mode 100644 dbapp/mainapp/views/map.py create mode 100644 dbapp/mainapp/views/objitem.py create mode 100644 dbapp/mainapp/views/source.py rename dbapp/mainapp/{views.py => views_old.py} (99%) 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 }}

-
- Назад -
-
-
- - -
-
-

Основная информация

-
-
-
-
- -
{{ object.name|default:"-" }}
-
-
-
-
- -
- {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} -
-
-
-
-
-
-
- -
- {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %} -
-
-
-
-
- -
- {% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} -
-
-
-
-
-
-
- -
- {% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %} -
-
-
-
-
- - -
-
-

ВЧ загрузка

-
- - {% if object.parameter_obj %} -
-
-
- -
{{ object.parameter_obj.id_satellite.name|default:"-" }}
-
-
-
-
- -
{{ object.parameter_obj.frequency|default:"-" }}
-
-
-
-
- -
{{ object.parameter_obj.freq_range|default:"-" }}
-
-
-
-
- -
{{ object.parameter_obj.polarization.name|default:"-" }}
-
-
-
-
-
-
- -
{{ object.parameter_obj.bod_velocity|default:"-" }}
-
-
-
-
- -
{{ object.parameter_obj.modulation.name|default:"-" }}
-
-
-
-
- -
{{ object.parameter_obj.snr|default:"-" }}
-
-
-
-
- -
{{ object.parameter_obj.standard.name|default:"-" }}
-
-
-
- {% else %} -
-

Нет данных о ВЧ загрузке

-
- {% endif %} -
- - -
-
-

Карта

-
-
-
-
-
- - -
-
-

Геоданные

-
- - {% if object.geo_obj %} - -
-
Координаты геолокации
-
-
-
- -
- {% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %} -
-
-
-
- -
- {% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %} -
-
-
-
- - -
-
Координаты Кубсата
-
-
-
- -
- {% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %} -
-
-
-
-
- -
- {% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %} -
-
-
-
-
- - -
-
Координаты оперативников
-
-
-
- -
- {% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %} -
-
-
-
-
- -
- {% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %} -
-
-
-
-
- -
-
-
- -
{{ object.geo_obj.location|default:"-" }}
-
-
-
-
- -
{{ object.geo_obj.comment|default:"-" }}
-
-
-
- -
-
-
- -
- {% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %} -
-
-
-
- -
- {% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %} -
-
-
-
- -
-
-
- -
- {% if object.geo_obj.distance_coords_kup is not None %} - {{ object.geo_obj.distance_coords_kup|floatformat:2 }} - {% else %} - - - {% endif %} -
-
-
-
-
- -
- {% if object.geo_obj.distance_coords_valid is not None %} - {{ object.geo_obj.distance_coords_valid|floatformat:2 }} - {% else %} - - - {% endif %} -
-
-
-
-
- -
- {% if object.geo_obj.distance_kup_valid is not None %} - {{ object.geo_obj.distance_kup_valid|floatformat:2 }} - {% else %} - - - {% endif %} -
-
-
-
- {% else %} -

Нет данных о геолокации

- {% endif %} -
-
- -
- {% 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 }}

+
+ Назад +
+
+
+ + +
+
+

Основная информация

+
+
+
+
+ +
{{ object.name|default:"-" }}
+
+
+
+
+ +
+ {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} +
+
+
+
+
+
+
+ +
+ {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %} +
+
+
+
+
+ +
+ {% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} +
+
+
+
+
+
+
+ +
+ {% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %} +
+
+
+
+
+ + +
+
+

ВЧ загрузка

+
+ + {% if object.parameter_obj %} +
+
+
+ +
{{ object.parameter_obj.id_satellite.name|default:"-" }}
+
+
+
+
+ +
{{ object.parameter_obj.frequency|default:"-" }}
+
+
+
+
+ +
{{ object.parameter_obj.freq_range|default:"-" }}
+
+
+
+
+ +
{{ object.parameter_obj.polarization.name|default:"-" }}
+
+
+
+
+
+
+ +
{{ object.parameter_obj.bod_velocity|default:"-" }}
+
+
+
+
+ +
{{ object.parameter_obj.modulation.name|default:"-" }}
+
+
+
+
+ +
{{ object.parameter_obj.snr|default:"-" }}
+
+
+
+
+ +
{{ object.parameter_obj.standard.name|default:"-" }}
+
+
+
+ {% else %} +
+

Нет данных о ВЧ загрузке

+
+ {% endif %} +
+ + +
+
+

Карта

+
+
+
+
+
+ + +
+
+

Геоданные

+
+ + {% if object.geo_obj %} + +
+
Координаты геолокации
+
+
+
+ +
+ {% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %} +
+
+
+
+ +
+ {% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %} +
+
+
+
+ +
+
+
+ +
{{ object.geo_obj.location|default:"-" }}
+
+
+
+
+ +
{{ object.geo_obj.comment|default:"-" }}
+
+
+
+ +
+
+
+ +
+ {% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %} +
+
+
+
+ +
+ {% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %} +
+
+
+
+ {% else %} +

Нет данных о геолокации

+ {% endif %} +
+
+ +
+ {% 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 %} - Назад -
-
-
- -
- {% csrf_token %} - - -
-
-

Основная информация

-
-
-
- {% include 'mainapp/components/_form_field.html' with field=form.name %} -
-
-
- -
- {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} -
-
-
-
-
-
-
- -
- {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %} -
-
-
-
-
- -
- {% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} -
-
-
-
-
-
-
- -
- {% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %} -
-
-
-
-
- - -
-
-

ВЧ загрузка

-
- -
-
-
- {% include 'mainapp/components/_form_field.html' with field=parameter_form.id_satellite %} -
-
- {% include 'mainapp/components/_form_field.html' with field=parameter_form.frequency %} -
-
- {% include 'mainapp/components/_form_field.html' with field=parameter_form.freq_range %} -
-
- {% include 'mainapp/components/_form_field.html' with field=parameter_form.polarization %} -
-
-
-
- {% include 'mainapp/components/_form_field.html' with field=parameter_form.bod_velocity %} -
-
- {% include 'mainapp/components/_form_field.html' with field=parameter_form.modulation %} -
-
- {% include 'mainapp/components/_form_field.html' with field=parameter_form.snr %} -
-
- {% include 'mainapp/components/_form_field.html' with field=parameter_form.standard %} -
-
-
-
- - -
-
-

Карта

-
-
-
-
-
- - -
-
-

Геоданные

-
- - - -
-
Координаты геолокации
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
Координаты Кубсата
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
Координаты оперативников
-
-
-
- - -
-
-
-
- - -
-
-
-
- -
-
- {% include 'mainapp/components/_form_field.html' with field=geo_form.location %} -
-
- {% include 'mainapp/components/_form_field.html' with field=geo_form.comment %} -
-
- -
-
-
- -
-
- - -
-
- - -
-
-
-
-
- {% include 'mainapp/components/_form_field.html' with field=geo_form.is_average %} -
-
- - {% if object.geo_obj %} -
-
-
- -
- {% if object.geo_obj.distance_coords_kup is not None %} - {{ object.geo_obj.distance_coords_kup|floatformat:2 }} - {% else %} - - - {% endif %} -
-
-
-
-
- -
- {% if object.geo_obj.distance_coords_valid is not None %} - {{ object.geo_obj.distance_coords_valid|floatformat:2 }} - {% else %} - - - {% endif %} -
-
-
-
-
- -
- {% if object.geo_obj.distance_kup_valid is not None %} - {{ object.geo_obj.distance_kup_valid|floatformat:2 }} - {% else %} - - - {% 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 %} + Назад +
+
+
+ +
+ {% csrf_token %} + + +
+
+

Основная информация

+
+
+
+ {% include 'mainapp/components/_form_field.html' with field=form.name %} +
+
+
+ +
+ {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} +
+
+
+
+
+
+
+ +
+ {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %} +
+
+
+
+
+ +
+ {% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} +
+
+
+
+
+
+
+ +
+ {% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %} +
+
+
+
+
+ + +
+
+

ВЧ загрузка

+
+ +
+
+
+ {% include 'mainapp/components/_form_field.html' with field=parameter_form.id_satellite %} +
+
+ {% include 'mainapp/components/_form_field.html' with field=parameter_form.frequency %} +
+
+ {% include 'mainapp/components/_form_field.html' with field=parameter_form.freq_range %} +
+
+ {% include 'mainapp/components/_form_field.html' with field=parameter_form.polarization %} +
+
+
+
+ {% include 'mainapp/components/_form_field.html' with field=parameter_form.bod_velocity %} +
+
+ {% include 'mainapp/components/_form_field.html' with field=parameter_form.modulation %} +
+
+ {% include 'mainapp/components/_form_field.html' with field=parameter_form.snr %} +
+
+ {% include 'mainapp/components/_form_field.html' with field=parameter_form.standard %} +
+
+
+
+ + +
+
+

Карта

+
+
+
+
+
+ + +
+
+

Геоданные

+
+ + + +
+
Координаты геолокации
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+ {% include 'mainapp/components/_form_field.html' with field=geo_form.location %} +
+
+ {% include 'mainapp/components/_form_field.html' with field=geo_form.comment %} +
+
+ +
+
+
+ +
+
+ + +
+
+ + +
+
+
+
+
+ {% include 'mainapp/components/_form_field.html' with field=geo_form.is_average %} +
+
+
+
+
+{% 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)