Реструктуризация views

This commit is contained in:
2025-11-13 16:11:37 +03:00
parent d0a53e251e
commit 122fe74e14
15 changed files with 2866 additions and 1346 deletions

View File

@@ -16,7 +16,7 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include 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 django.contrib.auth import views as auth_views
from debug_toolbar.toolbar import debug_toolbar_urls from debug_toolbar.toolbar import debug_toolbar_urls
@@ -26,5 +26,5 @@ urlpatterns = [
path('', include('mapsapp.urls', namespace='mapsapp')), path('', include('mapsapp.urls', namespace='mapsapp')),
# Authentication URLs # Authentication URLs
path('login/', auth_views.LoginView.as_view(), name='login'), 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() ] + debug_toolbar_urls()

View File

@@ -1,229 +1,209 @@
""" """
Переиспользуемые миксины для представлений mainapp. Переиспользуемые миксины для представлений mainapp.
Этот модуль содержит миксины для стандартизации общей логики в представлениях, Этот модуль содержит миксины для стандартизации общей логики в представлениях,
включая проверку прав доступа, обработку координат и сообщений. включая проверку прав доступа, обработку координат и сообщений.
""" """
# Standard library imports # Standard library imports
from datetime import datetime from datetime import datetime
from typing import Optional, Tuple from typing import Optional, Tuple
# Django imports # Django imports
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
class RoleRequiredMixin(UserPassesTestMixin): class RoleRequiredMixin(UserPassesTestMixin):
""" """
Mixin для проверки роли пользователя. Mixin для проверки роли пользователя.
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению. Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
Attributes: Attributes:
required_roles (list): Список допустимых ролей для доступа. required_roles (list): Список допустимых ролей для доступа.
По умолчанию ['admin', 'moderator']. По умолчанию ['admin', 'moderator'].
Example: Example:
class MyView(RoleRequiredMixin, View): class MyView(RoleRequiredMixin, View):
required_roles = ['admin', 'moderator'] required_roles = ['admin', 'moderator']
def get(self, request): def get(self, request):
# Только пользователи с ролью admin или moderator могут получить доступ # Только пользователи с ролью admin или moderator могут получить доступ
return render(request, 'template.html') return render(request, 'template.html')
""" """
required_roles = ["admin", "moderator"] required_roles = ["admin", "moderator"]
def test_func(self) -> bool: def test_func(self) -> bool:
""" """
Проверяет, имеет ли пользователь требуемую роль. Проверяет, имеет ли пользователь требуемую роль.
Returns: Returns:
bool: True если пользователь имеет одну из требуемых ролей, иначе False. bool: True если пользователь имеет одну из требуемых ролей, иначе False.
""" """
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
return False return False
if not hasattr(self.request.user, "customuser"): if not hasattr(self.request.user, "customuser"):
return False return False
return self.request.user.customuser.role in self.required_roles return self.request.user.customuser.role in self.required_roles
class CoordinateProcessingMixin: class CoordinateProcessingMixin:
""" """
Mixin для обработки координат из POST данных форм. Mixin для обработки координат из POST данных форм.
Предоставляет методы для извлечения и обработки координат различных типов Предоставляет методы для извлечения и обработки координат различных типов
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo. (геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
Example: Note: Координаты Кубсата и оперативников теперь хранятся в модели Source,
class MyFormView(CoordinateProcessingMixin, FormView): а не в модели Geo, но для совместимости в форме все еще могут быть поля
def form_valid(self, form): для этих координат.
geo_instance = Geo() """
self.process_coordinates(geo_instance)
geo_instance.save() def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
return super().form_valid(form) """
""" Обрабатывает координаты из POST данных и применяет их к объекту Geo.
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None: Извлекает координаты геолокации из POST запроса
""" и устанавливает соответствующие поля объекта Geo.
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
Args:
Извлекает координаты геолокации, кубсата и оперативников из POST запроса geo_instance: Экземпляр модели Geo для обновления координат.
и устанавливает соответствующие поля объекта Geo. prefix (str): Префикс для полей формы (по умолчанию 'geo').
Args: Note:
geo_instance: Экземпляр модели Geo для обновления координат. Метод ожидает следующие поля в request.POST:
prefix (str): Префикс для полей формы (по умолчанию 'geo'). - geo_longitude, geo_latitude: координаты геолокации
"""
Note: # Обрабатываем координаты геолокации
Метод ожидает следующие поля в request.POST: geo_coords = self._extract_coordinates("geo")
- geo_longitude, geo_latitude: координаты геолокации if geo_coords:
- kupsat_longitude, kupsat_latitude: координаты кубсата geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
- valid_longitude, valid_latitude: координаты оперативников
""" def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
# Обрабатываем координаты геолокации """
geo_coords = self._extract_coordinates("geo") Извлекает координаты указанного типа из POST данных.
if geo_coords:
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326) Args:
coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
# Обрабатываем координаты Кубсата
kupsat_coords = self._extract_coordinates("kupsat") Returns:
if kupsat_coords: Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
geo_instance.coords_kupsat = Point( если координаты не найдены или невалидны.
kupsat_coords[0], kupsat_coords[1], srid=4326 """
) longitude_key = f"{coord_type}_longitude"
latitude_key = f"{coord_type}_latitude"
# Обрабатываем координаты оперативников
valid_coords = self._extract_coordinates("valid") longitude = self.request.POST.get(longitude_key)
if valid_coords: latitude = self.request.POST.get(latitude_key)
geo_instance.coords_valid = Point(
valid_coords[0], valid_coords[1], srid=4326 if longitude and latitude:
) try:
return (float(longitude), float(latitude))
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]: except (ValueError, TypeError):
""" return None
Извлекает координаты указанного типа из POST данных. return None
Args: def process_timestamp(self, geo_instance) -> None:
coord_type (str): Тип координат ('geo', 'kupsat', 'valid'). """
Обрабатывает дату и время из POST данных и применяет к объекту Geo.
Returns:
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None, Args:
если координаты не найдены или невалидны. geo_instance: Экземпляр модели Geo для обновления timestamp.
"""
longitude_key = f"{coord_type}_longitude" Note:
latitude_key = f"{coord_type}_latitude" Метод ожидает следующие поля в request.POST:
- timestamp_date: дата в формате YYYY-MM-DD
longitude = self.request.POST.get(longitude_key) - timestamp_time: время в формате HH:MM
latitude = self.request.POST.get(latitude_key) """
timestamp_date = self.request.POST.get("timestamp_date")
if longitude and latitude: timestamp_time = self.request.POST.get("timestamp_time")
try:
return (float(longitude), float(latitude)) if timestamp_date and timestamp_time:
except (ValueError, TypeError): try:
return None naive_datetime = datetime.strptime(
return None f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
)
def process_timestamp(self, geo_instance) -> None: geo_instance.timestamp = naive_datetime
""" except ValueError:
Обрабатывает дату и время из POST данных и применяет к объекту Geo. # Если формат даты/времени неверный, пропускаем
pass
Args:
geo_instance: Экземпляр модели Geo для обновления timestamp.
class FormMessageMixin:
Note: """
Метод ожидает следующие поля в request.POST: Mixin для стандартизации сообщений об успехе и ошибках в формах.
- 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") Attributes:
success_message (str): Сообщение при успешной обработке формы.
if timestamp_date and timestamp_time: error_message (str): Сообщение при ошибке обработки формы.
try:
naive_datetime = datetime.strptime( Example:
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M" class MyFormView(FormMessageMixin, FormView):
) success_message = "Данные успешно сохранены!"
geo_instance.timestamp = naive_datetime error_message = "Ошибка при сохранении данных"
except ValueError:
# Если формат даты/времени неверный, пропускаем def form_valid(self, form):
pass # Автоматически добавит success_message
return super().form_valid(form)
"""
class FormMessageMixin:
""" success_message = "Операция выполнена успешно"
Mixin для стандартизации сообщений об успехе и ошибках в формах. error_message = "Произошла ошибка при обработке формы"
Автоматически добавляет сообщения пользователю при успешной или неуспешной def form_valid(self, form):
обработке формы. """
Обрабатывает валидную форму и добавляет сообщение об успехе.
Attributes:
success_message (str): Сообщение при успешной обработке формы. Args:
error_message (str): Сообщение при ошибке обработки формы. form: Валидная форма Django.
Example: Returns:
class MyFormView(FormMessageMixin, FormView): HttpResponse: Результат обработки родительского метода form_valid.
success_message = "Данные успешно сохранены!" """
error_message = "Ошибка при сохранении данных" if self.success_message:
messages.success(self.request, self.success_message)
def form_valid(self, form): return super().form_valid(form)
# Автоматически добавит success_message
return super().form_valid(form) def form_invalid(self, form):
""" """
Обрабатывает невалидную форму и добавляет сообщение об ошибке.
success_message = "Операция выполнена успешно"
error_message = "Произошла ошибка при обработке формы" Args:
form: Невалидная форма Django.
def form_valid(self, form):
""" Returns:
Обрабатывает валидную форму и добавляет сообщение об успехе. HttpResponse: Результат обработки родительского метода form_invalid.
"""
Args: if self.error_message:
form: Валидная форма Django. messages.error(self.request, self.error_message)
return super().form_invalid(form)
Returns:
HttpResponse: Результат обработки родительского метода form_valid. def get_success_message(self) -> str:
""" """
if self.success_message: Возвращает сообщение об успехе.
messages.success(self.request, self.success_message)
return super().form_valid(form) Может быть переопределен в подклассах для динамического формирования сообщения.
def form_invalid(self, form): Returns:
""" str: Сообщение об успехе.
Обрабатывает невалидную форму и добавляет сообщение об ошибке. """
return self.success_message
Args:
form: Невалидная форма Django. def get_error_message(self) -> str:
"""
Returns: Возвращает сообщение об ошибке.
HttpResponse: Результат обработки родительского метода form_invalid.
""" Может быть переопределен в подклассах для динамического формирования сообщения.
if self.error_message:
messages.error(self.request, self.error_message) Returns:
return super().form_invalid(form) str: Сообщение об ошибке.
"""
def get_success_message(self) -> str: return self.error_message
"""
Возвращает сообщение об успехе.
Может быть переопределен в подклассах для динамического формирования сообщения.
Returns:
str: Сообщение об успехе.
"""
return self.success_message
def get_error_message(self) -> str:
"""
Возвращает сообщение об ошибке.
Может быть переопределен в подклассах для динамического формирования сообщения.
Returns:
str: Сообщение об ошибке.
"""
return self.error_message

View File

@@ -1,473 +1,319 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% load static %} {% load static %}
{% load static leaflet_tags %} {% load static leaflet_tags %}
{% load l10n %} {% load l10n %}
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %} {% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
{% block extra_css %} {% block extra_css %}
<style> <style>
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; } .form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; } .form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
.btn-action { margin-right: 0.5rem; } .btn-action { margin-right: 0.5rem; }
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; } .readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; } .coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; } .coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
.form-check-input { margin-top: 0.25rem; } .form-check-input { margin-top: 0.25rem; }
.datetime-group { display: flex; gap: 1rem; } .datetime-group { display: flex; gap: 1rem; }
.datetime-group > div { flex: 1; } .datetime-group > div { flex: 1; }
#map { height: 500px; width: 100%; margin-bottom: 1rem; } #map { height: 500px; width: 100%; margin-bottom: 1rem; }
.map-container { margin-bottom: 1rem; } .map-container { margin-bottom: 1rem; }
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; } .coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
.map-controls { .map-controls {
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-bottom: 1rem; margin-bottom: 1rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.map-control-btn { .map-control-btn {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
border: 1px solid #ced4da; border: 1px solid #ced4da;
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 0.25rem; border-radius: 0.25rem;
cursor: pointer; cursor: pointer;
} }
.map-control-btn.active { .map-control-btn.active {
background-color: #e9ecef; background-color: #e9ecef;
border-color: #dee2e6; border-color: #dee2e6;
} }
.map-control-btn.edit { .map-control-btn.edit {
background-color: #fff3cd; background-color: #fff3cd;
border-color: #ffeeba; border-color: #ffeeba;
} }
.map-control-btn.save { .map-control-btn.save {
background-color: #d1ecf1; background-color: #d1ecf1;
border-color: #bee5eb; border-color: #bee5eb;
} }
.map-control-btn.cancel { .map-control-btn.cancel {
background-color: #f8d7da; background-color: #f8d7da;
border-color: #f5c6cb; border-color: #f5c6cb;
} }
.leaflet-marker-icon { .leaflet-marker-icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3)); filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
} }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center"> <div class="col-12 d-flex justify-content-between align-items-center">
<h2>Просмотр объекта: {{ object.name }}</h2> <h2>Просмотр объекта: {{ object.name }}</h2>
<div> <div>
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a> <a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
</div> </div>
</div> </div>
</div> </div>
<!-- Основная информация --> <!-- Основная информация -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>Основная информация</h4> <h4>Основная информация</h4>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Название:</label> <label class="form-label">Название:</label>
<div class="readonly-field">{{ object.name|default:"-" }}</div> <div class="readonly-field">{{ object.name|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Дата создания:</label> <label class="form-label">Дата создания:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} {% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Создан пользователем:</label> <label class="form-label">Создан пользователем:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %} {% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Дата последнего изменения:</label> <label class="form-label">Дата последнего изменения:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %} {% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Изменен пользователем:</label> <label class="form-label">Изменен пользователем:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %} {% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- ВЧ загрузка --> <!-- ВЧ загрузка -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>ВЧ загрузка</h4> <h4>ВЧ загрузка</h4>
</div> </div>
{% if object.parameter_obj %} {% if object.parameter_obj %}
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Спутник:</label> <label class="form-label">Спутник:</label>
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Частота (МГц):</label> <label class="form-label">Частота (МГц):</label>
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Полоса (МГц):</label> <label class="form-label">Полоса (МГц):</label>
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Поляризация:</label> <label class="form-label">Поляризация:</label>
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Символьная скорость:</label> <label class="form-label">Символьная скорость:</label>
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Модуляция:</label> <label class="form-label">Модуляция:</label>
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">ОСШ:</label> <label class="form-label">ОСШ:</label>
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Стандарт:</label> <label class="form-label">Стандарт:</label>
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div> <div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
</div> </div>
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="mb-3"> <div class="mb-3">
<p>Нет данных о ВЧ загрузке</p> <p>Нет данных о ВЧ загрузке</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- Блок с картой --> <!-- Блок с картой -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>Карта</h4> <h4>Карта</h4>
</div> </div>
<div class="map-container"> <div class="map-container">
<div id="map"></div> <div id="map"></div>
</div> </div>
</div> </div>
<!-- Геоданные --> <!-- Геоданные -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header"> <div class="form-section-header">
<h4>Геоданные</h4> <h4>Геоданные</h4>
</div> </div>
{% if object.geo_obj %} {% if object.geo_obj %}
<!-- Координаты геолокации --> <!-- Координаты геолокации -->
<div class="coord-sync-group"> <div class="coord-sync-group">
<div class="coord-group-header">Координаты геолокации</div> <div class="coord-group-header">Координаты геолокации</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Широта:</label> <label class="form-label">Широта:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %} {% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Долгота:</label> <label class="form-label">Долгота:</label>
<div class="readonly-field"> <div class="readonly-field">
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %} {% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Координаты Кубсата --> <div class="row">
<div class="coord-group"> <div class="col-md-6">
<div class="coord-group-header">Координаты Кубсата</div> <div class="mb-3">
<div class="row"> <label class="form-label">Местоположение:</label>
<div class="col-md-6"> <div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
<div class="mb-3"> </div>
<label class="form-label">Долгота:</label> </div>
<div class="readonly-field"> <div class="col-md-6">
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %} <div class="mb-3">
</div> <label class="form-label">Комментарий:</label>
</div> <div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
</div> </div>
<div class="col-md-6"> </div>
<div class="mb-3"> </div>
<label class="form-label">Широта:</label>
<div class="readonly-field"> <div class="row">
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %} <div class="col-md-6">
</div> <div class="mb-3">
</div> <label class="form-label">Дата и время:</label>
</div> <div class="readonly-field">
</div> {% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div> </div>
</div>
<!-- Координаты оперативников --> <div class="col-md-6">
<div class="coord-group"> <div class="mb-3">
<div class="coord-group-header">Координаты оперативников</div> <label class="form-check-label">Усредненное значение:</label>
<div class="row"> <div class="readonly-field">
<div class="col-md-6"> {% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
<div class="mb-3"> </div>
<label class="form-label">Долгота:</label> </div>
<div class="readonly-field"> </div>
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %} </div>
</div> {% else %}
</div> <p>Нет данных о геолокации</p>
</div> {% endif %}
<div class="col-md-6"> </div>
<div class="mb-3"> </div>
<label class="form-label">Широта:</label>
<div class="readonly-field"> <div class="d-flex justify-content-end mt-4">
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %} {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
</div> <a href="{% url 'mainapp:objitem_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-action">Редактировать</a>
</div> {% endif %}
</div> <a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
</div> </div>
</div> </div>
{% endblock %}
<div class="row">
<div class="col-md-6"> {% block extra_js %}
<div class="mb-3"> {{ block.super }}
<label class="form-label">Местоположение:</label> <!-- Подключаем Leaflet и его плагины -->
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div> {% leaflet_js %}
</div> {% leaflet_css %}
</div> <script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<div class="col-md-6">
<div class="mb-3"> <script>
<label class="form-label">Комментарий:</label> document.addEventListener('DOMContentLoaded', function() {
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div> // Инициализация карты
</div> const map = L.map('map').setView([55.75, 37.62], 5);
</div>
</div> L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
<div class="row"> }).addTo(map);
<div class="col-md-6">
<div class="mb-3"> // Функция для создания иконки маркера
<label class="form-label">Дата и время:</label> function createMarkerIcon() {
<div class="readonly-field"> return L.icon({
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %} iconUrl: '{% static "leaflet-markers/img/marker-icon-blue.png" %}',
</div> shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
</div> iconSize: [25, 41],
<div class="col-md-6"> iconAnchor: [12, 41],
<div class="mb-3"> popupAnchor: [1, -34],
<label class="form-check-label">Усредненное значение:</label> shadowSize: [41, 41]
<div class="readonly-field"> });
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %} }
</div>
</div> // Получаем координаты из данных объекта
</div> {% if object.geo_obj and object.geo_obj.coords %}
</div> const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
const geoLng = {{ object.geo_obj.coords.x|unlocalize }};
<div class="row mt-3"> {% else %}
<div class="col-md-4"> const geoLat = 55.75;
<div class="mb-3"> const geoLng = 37.62;
<label class="form-label">Расстояние гео-кубсат, км:</label> {% endif %}
<div class="readonly-field">
{% if object.geo_obj.distance_coords_kup is not None %} // Создаем маркер геолокации
{{ object.geo_obj.distance_coords_kup|floatformat:2 }} const marker = L.marker([geoLat, geoLng], {
{% else %} draggable: false,
- icon: createMarkerIcon(),
{% endif %} title: 'Геолокация'
</div> }).addTo(map);
</div> marker.bindPopup('Геолокация');
</div>
<div class="col-md-4"> // Центрируем карту на маркере
<div class="mb-3"> map.setView(marker.getLatLng(), 10);
<label class="form-label">Расстояние гео-опер, км:</label> });
<div class="readonly-field"> </script>
{% if object.geo_obj.distance_coords_valid is not None %}
{{ object.geo_obj.distance_coords_valid|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние кубсат-опер, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_kup_valid is not None %}
{{ object.geo_obj.distance_kup_valid|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<p>Нет данных о геолокации</p>
{% endif %}
</div>
</div>
<div class="d-flex justify-content-end mt-4">
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<a href="{% url 'mainapp:objitem_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-action">Редактировать</a>
{% endif %}
<a href="{% url 'mainapp:objitem_list' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-secondary btn-action">Назад</a>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<!-- Подключаем Leaflet и его плагины -->
{% leaflet_js %}
{% leaflet_css %}
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Инициализация карты
const map = L.map('map').setView([55.75, 37.62], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Определяем цвета для маркеров
const colors = {
geo: 'blue',
kupsat: 'red',
valid: 'green'
};
// Функция для создания иконки маркера
function createMarkerIcon(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
}
// Маркеры
const markers = {};
function createMarker(position, color, name) {
const marker = L.marker(position, {
draggable: false,
icon: createMarkerIcon(color),
title: name
}).addTo(map);
marker.bindPopup(name);
return marker;
}
// Получаем координаты из данных объекта
{% if object.geo_obj and object.geo_obj.coords %}
const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
const geoLng = {{ object.geo_obj.coords.x|unlocalize }};
{% else %}
const geoLat = 55.75;
const geoLng = 37.62;
{% endif %}
{% if object.geo_obj and object.geo_obj.coords_kupsat %}
const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }};
const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }};
{% else %}
const kupsatLat = 55.75;
const kupsatLng = 37.61;
{% endif %}
{% if object.geo_obj and object.geo_obj.coords_valid %}
const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }};
const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }};
{% else %}
const validLat = 55.75;
const validLng = 37.63;
{% endif %}
// Создаем маркеры
markers.geo = createMarker(
[geoLat, geoLng],
colors.geo,
'Геолокация'
);
markers.kupsat = createMarker(
[kupsatLat, kupsatLng],
colors.kupsat,
'Кубсат'
);
markers.valid = createMarker(
[validLat, validLng],
colors.valid,
'Оперативник'
);
// Центрируем карту на первом маркере
if (map.hasLayer(markers.geo)) {
map.setView(markers.geo.getLatLng(), 10);
}
// Легенда
const legend = L.control({ position: 'bottomright' });
legend.onAdd = function() {
const div = L.DomUtil.create('div', 'info legend');
div.style.fontSize = '14px';
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.borderRadius = '4px';
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
div.innerHTML = `
<h5>Легенда</h5>
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
<div><span style="color: red; font-weight: bold;">•</span> Кубсат</div>
<div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
`;
return div;
};
legend.addTo(map);
});
</script>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,67 @@
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.urls import path 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' app_name = 'mainapp'
urlpatterns = [ urlpatterns = [
path('', views.SourceListView.as_view(), name='home'), path('', SourceListView.as_view(), name='home'),
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
path('actions/', views.ActionsPageView.as_view(), name='actions'), path('actions/', ActionsPageView.as_view(), name='actions'),
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'), path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'), path('satellites', AddSatellitesView.as_view(), name='add_sats'),
path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'), path('api/locations/<int:sat_id>/geojson/', GetLocationsView.as_view(), name='locations_by_id'),
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'), path('transponders', AddTranspondersView.as_view(), name='add_trans'),
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'), path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'), path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'), path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'), path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', views.ClusterTestView.as_view(), name='cluster'), path('cluster/', ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'), path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'), path('vch-link/', LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'), path('link-lyngsat/', LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
path('api/lyngsat/<int:lyngsat_id>/', views.LyngsatDataAPIView.as_view(), name='lyngsat_data_api'), path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
path('api/sigma-parameter/<int:parameter_id>/', views.SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'), path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
path('api/source/<int:source_id>/objitems/', views.SourceObjItemsAPIView.as_view(), name='source_objitems_api'), path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'), path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'), path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'), path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'), path('object/<int:pk>/', ObjItemDetailView.as_view(), name='objitem_detail'),
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'), path('object/<int:pk>/delete/', ObjItemDeleteView.as_view(), name='objitem_delete'),
path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'), path('fill-lyngsat-data/', FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'), path('lyngsat-task-status/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'), path('lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
path('api/lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'), path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
path('clear-lyngsat-cache/', views.ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'), path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
path('logout/', custom_logout, name='logout'),
] ]

View File

@@ -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

View File

@@ -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',
]

301
dbapp/mainapp/views/api.py Normal file
View File

@@ -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)

View File

@@ -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")

View File

@@ -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")

View File

@@ -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')

139
dbapp/mainapp/views/map.py Normal file
View File

@@ -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": "ок"})

View File

@@ -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)

View File

@@ -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)

View File

@@ -61,15 +61,6 @@ class AddSatellitesView(LoginRequiredMixin, View):
return redirect("mainapp:home") 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): class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
template_name = "mainapp/transponders_upload.html" template_name = "mainapp/transponders_upload.html"
form_class = UploadFileForm form_class = UploadFileForm
@@ -1414,9 +1405,6 @@ class ObjItemFormView(
geo_instance.comment = geo_form.cleaned_data["comment"] geo_instance.comment = geo_form.cleaned_data["comment"]
geo_instance.is_average = geo_form.cleaned_data["is_average"] geo_instance.is_average = geo_form.cleaned_data["is_average"]
# Обрабатываем координаты
self.process_coordinates(geo_instance)
# Обрабатываем дату/время # Обрабатываем дату/время
self.process_timestamp(geo_instance) self.process_timestamp(geo_instance)