Реструктуризация views
This commit is contained in:
@@ -16,7 +16,7 @@ Including another URLconf
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from mainapp import views
|
||||
from mainapp.views import custom_logout
|
||||
from django.contrib.auth import views as auth_views
|
||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
|
||||
@@ -26,5 +26,5 @@ urlpatterns = [
|
||||
path('', include('mapsapp.urls', namespace='mapsapp')),
|
||||
# Authentication URLs
|
||||
path('login/', auth_views.LoginView.as_view(), name='login'),
|
||||
path('logout/', views.custom_logout, name='logout'),
|
||||
path('logout/', custom_logout, name='logout'),
|
||||
] + debug_toolbar_urls()
|
||||
|
||||
@@ -1,229 +1,209 @@
|
||||
"""
|
||||
Переиспользуемые миксины для представлений mainapp.
|
||||
|
||||
Этот модуль содержит миксины для стандартизации общей логики в представлениях,
|
||||
включая проверку прав доступа, обработку координат и сообщений.
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Django imports
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
|
||||
class RoleRequiredMixin(UserPassesTestMixin):
|
||||
"""
|
||||
Mixin для проверки роли пользователя.
|
||||
|
||||
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
|
||||
|
||||
Attributes:
|
||||
required_roles (list): Список допустимых ролей для доступа.
|
||||
По умолчанию ['admin', 'moderator'].
|
||||
|
||||
Example:
|
||||
class MyView(RoleRequiredMixin, View):
|
||||
required_roles = ['admin', 'moderator']
|
||||
|
||||
def get(self, request):
|
||||
# Только пользователи с ролью admin или moderator могут получить доступ
|
||||
return render(request, 'template.html')
|
||||
"""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def test_func(self) -> bool:
|
||||
"""
|
||||
Проверяет, имеет ли пользователь требуемую роль.
|
||||
|
||||
Returns:
|
||||
bool: True если пользователь имеет одну из требуемых ролей, иначе False.
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if not hasattr(self.request.user, "customuser"):
|
||||
return False
|
||||
|
||||
return self.request.user.customuser.role in self.required_roles
|
||||
|
||||
|
||||
class CoordinateProcessingMixin:
|
||||
"""
|
||||
Mixin для обработки координат из POST данных форм.
|
||||
|
||||
Предоставляет методы для извлечения и обработки координат различных типов
|
||||
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
|
||||
|
||||
Example:
|
||||
class MyFormView(CoordinateProcessingMixin, FormView):
|
||||
def form_valid(self, form):
|
||||
geo_instance = Geo()
|
||||
self.process_coordinates(geo_instance)
|
||||
geo_instance.save()
|
||||
return super().form_valid(form)
|
||||
"""
|
||||
|
||||
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
|
||||
"""
|
||||
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
|
||||
|
||||
Извлекает координаты геолокации, кубсата и оперативников из POST запроса
|
||||
и устанавливает соответствующие поля объекта Geo.
|
||||
|
||||
Args:
|
||||
geo_instance: Экземпляр модели Geo для обновления координат.
|
||||
prefix (str): Префикс для полей формы (по умолчанию 'geo').
|
||||
|
||||
Note:
|
||||
Метод ожидает следующие поля в request.POST:
|
||||
- geo_longitude, geo_latitude: координаты геолокации
|
||||
- kupsat_longitude, kupsat_latitude: координаты кубсата
|
||||
- valid_longitude, valid_latitude: координаты оперативников
|
||||
"""
|
||||
# Обрабатываем координаты геолокации
|
||||
geo_coords = self._extract_coordinates("geo")
|
||||
if geo_coords:
|
||||
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
|
||||
|
||||
# Обрабатываем координаты Кубсата
|
||||
kupsat_coords = self._extract_coordinates("kupsat")
|
||||
if kupsat_coords:
|
||||
geo_instance.coords_kupsat = Point(
|
||||
kupsat_coords[0], kupsat_coords[1], srid=4326
|
||||
)
|
||||
|
||||
# Обрабатываем координаты оперативников
|
||||
valid_coords = self._extract_coordinates("valid")
|
||||
if valid_coords:
|
||||
geo_instance.coords_valid = Point(
|
||||
valid_coords[0], valid_coords[1], srid=4326
|
||||
)
|
||||
|
||||
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
|
||||
"""
|
||||
Извлекает координаты указанного типа из POST данных.
|
||||
|
||||
Args:
|
||||
coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
|
||||
|
||||
Returns:
|
||||
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
|
||||
если координаты не найдены или невалидны.
|
||||
"""
|
||||
longitude_key = f"{coord_type}_longitude"
|
||||
latitude_key = f"{coord_type}_latitude"
|
||||
|
||||
longitude = self.request.POST.get(longitude_key)
|
||||
latitude = self.request.POST.get(latitude_key)
|
||||
|
||||
if longitude and latitude:
|
||||
try:
|
||||
return (float(longitude), float(latitude))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return None
|
||||
|
||||
def process_timestamp(self, geo_instance) -> None:
|
||||
"""
|
||||
Обрабатывает дату и время из POST данных и применяет к объекту Geo.
|
||||
|
||||
Args:
|
||||
geo_instance: Экземпляр модели Geo для обновления timestamp.
|
||||
|
||||
Note:
|
||||
Метод ожидает следующие поля в request.POST:
|
||||
- timestamp_date: дата в формате YYYY-MM-DD
|
||||
- timestamp_time: время в формате HH:MM
|
||||
"""
|
||||
timestamp_date = self.request.POST.get("timestamp_date")
|
||||
timestamp_time = self.request.POST.get("timestamp_time")
|
||||
|
||||
if timestamp_date and timestamp_time:
|
||||
try:
|
||||
naive_datetime = datetime.strptime(
|
||||
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
|
||||
)
|
||||
geo_instance.timestamp = naive_datetime
|
||||
except ValueError:
|
||||
# Если формат даты/времени неверный, пропускаем
|
||||
pass
|
||||
|
||||
|
||||
class FormMessageMixin:
|
||||
"""
|
||||
Mixin для стандартизации сообщений об успехе и ошибках в формах.
|
||||
|
||||
Автоматически добавляет сообщения пользователю при успешной или неуспешной
|
||||
обработке формы.
|
||||
|
||||
Attributes:
|
||||
success_message (str): Сообщение при успешной обработке формы.
|
||||
error_message (str): Сообщение при ошибке обработки формы.
|
||||
|
||||
Example:
|
||||
class MyFormView(FormMessageMixin, FormView):
|
||||
success_message = "Данные успешно сохранены!"
|
||||
error_message = "Ошибка при сохранении данных"
|
||||
|
||||
def form_valid(self, form):
|
||||
# Автоматически добавит success_message
|
||||
return super().form_valid(form)
|
||||
"""
|
||||
|
||||
success_message = "Операция выполнена успешно"
|
||||
error_message = "Произошла ошибка при обработке формы"
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Обрабатывает валидную форму и добавляет сообщение об успехе.
|
||||
|
||||
Args:
|
||||
form: Валидная форма Django.
|
||||
|
||||
Returns:
|
||||
HttpResponse: Результат обработки родительского метода form_valid.
|
||||
"""
|
||||
if self.success_message:
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
"""
|
||||
Обрабатывает невалидную форму и добавляет сообщение об ошибке.
|
||||
|
||||
Args:
|
||||
form: Невалидная форма Django.
|
||||
|
||||
Returns:
|
||||
HttpResponse: Результат обработки родительского метода form_invalid.
|
||||
"""
|
||||
if self.error_message:
|
||||
messages.error(self.request, self.error_message)
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_message(self) -> str:
|
||||
"""
|
||||
Возвращает сообщение об успехе.
|
||||
|
||||
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||
|
||||
Returns:
|
||||
str: Сообщение об успехе.
|
||||
"""
|
||||
return self.success_message
|
||||
|
||||
def get_error_message(self) -> str:
|
||||
"""
|
||||
Возвращает сообщение об ошибке.
|
||||
|
||||
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||
|
||||
Returns:
|
||||
str: Сообщение об ошибке.
|
||||
"""
|
||||
return self.error_message
|
||||
"""
|
||||
Переиспользуемые миксины для представлений mainapp.
|
||||
|
||||
Этот модуль содержит миксины для стандартизации общей логики в представлениях,
|
||||
включая проверку прав доступа, обработку координат и сообщений.
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Django imports
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
|
||||
class RoleRequiredMixin(UserPassesTestMixin):
|
||||
"""
|
||||
Mixin для проверки роли пользователя.
|
||||
|
||||
Проверяет, что пользователь имеет одну из требуемых ролей для доступа к представлению.
|
||||
|
||||
Attributes:
|
||||
required_roles (list): Список допустимых ролей для доступа.
|
||||
По умолчанию ['admin', 'moderator'].
|
||||
|
||||
Example:
|
||||
class MyView(RoleRequiredMixin, View):
|
||||
required_roles = ['admin', 'moderator']
|
||||
|
||||
def get(self, request):
|
||||
# Только пользователи с ролью admin или moderator могут получить доступ
|
||||
return render(request, 'template.html')
|
||||
"""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def test_func(self) -> bool:
|
||||
"""
|
||||
Проверяет, имеет ли пользователь требуемую роль.
|
||||
|
||||
Returns:
|
||||
bool: True если пользователь имеет одну из требуемых ролей, иначе False.
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if not hasattr(self.request.user, "customuser"):
|
||||
return False
|
||||
|
||||
return self.request.user.customuser.role in self.required_roles
|
||||
|
||||
|
||||
class CoordinateProcessingMixin:
|
||||
"""
|
||||
Mixin для обработки координат из POST данных форм.
|
||||
|
||||
Предоставляет методы для извлечения и обработки координат различных типов
|
||||
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
|
||||
|
||||
Note: Координаты Кубсата и оперативников теперь хранятся в модели Source,
|
||||
а не в модели Geo, но для совместимости в форме все еще могут быть поля
|
||||
для этих координат.
|
||||
"""
|
||||
|
||||
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
|
||||
"""
|
||||
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
|
||||
|
||||
Извлекает координаты геолокации из POST запроса
|
||||
и устанавливает соответствующие поля объекта Geo.
|
||||
|
||||
Args:
|
||||
geo_instance: Экземпляр модели Geo для обновления координат.
|
||||
prefix (str): Префикс для полей формы (по умолчанию 'geo').
|
||||
|
||||
Note:
|
||||
Метод ожидает следующие поля в request.POST:
|
||||
- geo_longitude, geo_latitude: координаты геолокации
|
||||
"""
|
||||
# Обрабатываем координаты геолокации
|
||||
geo_coords = self._extract_coordinates("geo")
|
||||
if geo_coords:
|
||||
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
|
||||
|
||||
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
|
||||
"""
|
||||
Извлекает координаты указанного типа из POST данных.
|
||||
|
||||
Args:
|
||||
coord_type (str): Тип координат ('geo', 'kupsat', 'valid').
|
||||
|
||||
Returns:
|
||||
Optional[Tuple[float, float]]: Кортеж (longitude, latitude) или None,
|
||||
если координаты не найдены или невалидны.
|
||||
"""
|
||||
longitude_key = f"{coord_type}_longitude"
|
||||
latitude_key = f"{coord_type}_latitude"
|
||||
|
||||
longitude = self.request.POST.get(longitude_key)
|
||||
latitude = self.request.POST.get(latitude_key)
|
||||
|
||||
if longitude and latitude:
|
||||
try:
|
||||
return (float(longitude), float(latitude))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return None
|
||||
|
||||
def process_timestamp(self, geo_instance) -> None:
|
||||
"""
|
||||
Обрабатывает дату и время из POST данных и применяет к объекту Geo.
|
||||
|
||||
Args:
|
||||
geo_instance: Экземпляр модели Geo для обновления timestamp.
|
||||
|
||||
Note:
|
||||
Метод ожидает следующие поля в request.POST:
|
||||
- timestamp_date: дата в формате YYYY-MM-DD
|
||||
- timestamp_time: время в формате HH:MM
|
||||
"""
|
||||
timestamp_date = self.request.POST.get("timestamp_date")
|
||||
timestamp_time = self.request.POST.get("timestamp_time")
|
||||
|
||||
if timestamp_date and timestamp_time:
|
||||
try:
|
||||
naive_datetime = datetime.strptime(
|
||||
f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M"
|
||||
)
|
||||
geo_instance.timestamp = naive_datetime
|
||||
except ValueError:
|
||||
# Если формат даты/времени неверный, пропускаем
|
||||
pass
|
||||
|
||||
|
||||
class FormMessageMixin:
|
||||
"""
|
||||
Mixin для стандартизации сообщений об успехе и ошибках в формах.
|
||||
|
||||
Автоматически добавляет сообщения пользователю при успешной или неуспешной
|
||||
обработке формы.
|
||||
|
||||
Attributes:
|
||||
success_message (str): Сообщение при успешной обработке формы.
|
||||
error_message (str): Сообщение при ошибке обработки формы.
|
||||
|
||||
Example:
|
||||
class MyFormView(FormMessageMixin, FormView):
|
||||
success_message = "Данные успешно сохранены!"
|
||||
error_message = "Ошибка при сохранении данных"
|
||||
|
||||
def form_valid(self, form):
|
||||
# Автоматически добавит success_message
|
||||
return super().form_valid(form)
|
||||
"""
|
||||
|
||||
success_message = "Операция выполнена успешно"
|
||||
error_message = "Произошла ошибка при обработке формы"
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
Обрабатывает валидную форму и добавляет сообщение об успехе.
|
||||
|
||||
Args:
|
||||
form: Валидная форма Django.
|
||||
|
||||
Returns:
|
||||
HttpResponse: Результат обработки родительского метода form_valid.
|
||||
"""
|
||||
if self.success_message:
|
||||
messages.success(self.request, self.success_message)
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
"""
|
||||
Обрабатывает невалидную форму и добавляет сообщение об ошибке.
|
||||
|
||||
Args:
|
||||
form: Невалидная форма Django.
|
||||
|
||||
Returns:
|
||||
HttpResponse: Результат обработки родительского метода form_invalid.
|
||||
"""
|
||||
if self.error_message:
|
||||
messages.error(self.request, self.error_message)
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_message(self) -> str:
|
||||
"""
|
||||
Возвращает сообщение об успехе.
|
||||
|
||||
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||
|
||||
Returns:
|
||||
str: Сообщение об успехе.
|
||||
"""
|
||||
return self.success_message
|
||||
|
||||
def get_error_message(self) -> str:
|
||||
"""
|
||||
Возвращает сообщение об ошибке.
|
||||
|
||||
Может быть переопределен в подклассах для динамического формирования сообщения.
|
||||
|
||||
Returns:
|
||||
str: Сообщение об ошибке.
|
||||
"""
|
||||
return self.error_message
|
||||
|
||||
@@ -1,473 +1,319 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load static leaflet_tags %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.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; }
|
||||
.btn-action { margin-right: 0.5rem; }
|
||||
.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-header { font-weight: bold; margin-bottom: 0.5rem; }
|
||||
.form-check-input { margin-top: 0.25rem; }
|
||||
.datetime-group { display: flex; gap: 1rem; }
|
||||
.datetime-group > div { flex: 1; }
|
||||
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
|
||||
.map-container { margin-bottom: 1rem; }
|
||||
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
|
||||
.map-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.map-control-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.map-control-btn.active {
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
.map-control-btn.edit {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
.map-control-btn.save {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
.map-control-btn.cancel {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
.leaflet-marker-icon {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<h2>Просмотр объекта: {{ object.name }}</h2>
|
||||
<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 class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Основная информация</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Название:</label>
|
||||
<div class="readonly-field">{{ object.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата создания:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Создан пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата последнего изменения:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Изменен пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ВЧ загрузка -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>ВЧ загрузка</h4>
|
||||
</div>
|
||||
|
||||
{% if object.parameter_obj %}
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Частота (МГц):</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Полоса (МГц):</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Поляризация:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Символьная скорость:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Модуляция:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ОСШ:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Стандарт:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-3">
|
||||
<p>Нет данных о ВЧ загрузке</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Блок с картой -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Карта</h4>
|
||||
</div>
|
||||
<div class="map-container">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Геоданные -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Геоданные</h4>
|
||||
</div>
|
||||
|
||||
{% if object.geo_obj %}
|
||||
<!-- Координаты геолокации -->
|
||||
<div class="coord-sync-group">
|
||||
<div class="coord-group-header">Координаты геолокации</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты Кубсата -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты Кубсата</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты оперативников -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты оперативников</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Местоположение:</label>
|
||||
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Комментарий:</label>
|
||||
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата и время:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label">Усредненное значение:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.is_average %}Да{% else %}Нет{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Расстояние гео-кубсат, км:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.distance_coords_kup is not None %}
|
||||
{{ object.geo_obj.distance_coords_kup|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_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: '© <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>
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load static leaflet_tags %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}Просмотр объекта: {{ object.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.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; }
|
||||
.btn-action { margin-right: 0.5rem; }
|
||||
.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-header { font-weight: bold; margin-bottom: 0.5rem; }
|
||||
.form-check-input { margin-top: 0.25rem; }
|
||||
.datetime-group { display: flex; gap: 1rem; }
|
||||
.datetime-group > div { flex: 1; }
|
||||
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
|
||||
.map-container { margin-bottom: 1rem; }
|
||||
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
|
||||
.map-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.map-control-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.map-control-btn.active {
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
.map-control-btn.edit {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
.map-control-btn.save {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
.map-control-btn.cancel {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
.leaflet-marker-icon {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<h2>Просмотр объекта: {{ object.name }}</h2>
|
||||
<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 class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Основная информация</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Название:</label>
|
||||
<div class="readonly-field">{{ object.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата создания:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Создан пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата последнего изменения:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Изменен пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ВЧ загрузка -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>ВЧ загрузка</h4>
|
||||
</div>
|
||||
|
||||
{% if object.parameter_obj %}
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Спутник:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.id_satellite.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Частота (МГц):</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.frequency|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Полоса (МГц):</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.freq_range|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Поляризация:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.polarization.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Символьная скорость:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.bod_velocity|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Модуляция:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.modulation.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ОСШ:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.snr|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Стандарт:</label>
|
||||
<div class="readonly-field">{{ object.parameter_obj.standard.name|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-3">
|
||||
<p>Нет данных о ВЧ загрузке</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Блок с картой -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Карта</h4>
|
||||
</div>
|
||||
<div class="map-container">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Геоданные -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Геоданные</h4>
|
||||
</div>
|
||||
|
||||
{% if object.geo_obj %}
|
||||
<!-- Координаты геолокации -->
|
||||
<div class="coord-sync-group">
|
||||
<div class="coord-group-header">Координаты геолокации</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Широта:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.y|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Долгота:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.coords %}{{ object.geo_obj.coords.x|floatformat:6 }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Местоположение:</label>
|
||||
<div class="readonly-field">{{ object.geo_obj.location|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Комментарий:</label>
|
||||
<div class="readonly-field">{{ object.geo_obj.comment|default:"-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата и время:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-check-label">Усредненное значение:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.geo_obj.is_average %}Да{% 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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Функция для создания иконки маркера
|
||||
function createMarkerIcon() {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-blue.png" %}',
|
||||
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем координаты из данных объекта
|
||||
{% 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 %}
|
||||
|
||||
// Создаем маркер геолокации
|
||||
const marker = L.marker([geoLat, geoLng], {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon(),
|
||||
title: 'Геолокация'
|
||||
}).addTo(map);
|
||||
marker.bindPopup('Геолокация');
|
||||
|
||||
// Центрируем карту на маркере
|
||||
map.setView(marker.getLatLng(), 10);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,67 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from .views import (
|
||||
ActionsPageView,
|
||||
AddSatellitesView,
|
||||
AddTranspondersView,
|
||||
ClusterTestView,
|
||||
ClearLyngsatCacheView,
|
||||
DeleteSelectedObjectsView,
|
||||
FillLyngsatDataView,
|
||||
GetLocationsView,
|
||||
LinkLyngsatSourcesView,
|
||||
LinkVchSigmaView,
|
||||
LoadCsvDataView,
|
||||
LoadExcelDataView,
|
||||
LyngsatDataAPIView,
|
||||
LyngsatTaskStatusAPIView,
|
||||
LyngsatTaskStatusView,
|
||||
ObjItemCreateView,
|
||||
ObjItemDeleteView,
|
||||
ObjItemDetailView,
|
||||
ObjItemListView,
|
||||
ObjItemUpdateView,
|
||||
ProcessKubsatView,
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
SourceListView,
|
||||
SourceObjItemsAPIView,
|
||||
SigmaParameterDataAPIView,
|
||||
UploadVchLoadView,
|
||||
custom_logout,
|
||||
)
|
||||
|
||||
app_name = 'mainapp'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.SourceListView.as_view(), name='home'),
|
||||
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'),
|
||||
path('actions/', views.ActionsPageView.as_view(), name='actions'),
|
||||
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'),
|
||||
path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
|
||||
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
|
||||
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
|
||||
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||
path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
|
||||
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
|
||||
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
|
||||
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
|
||||
path('api/lyngsat/<int:lyngsat_id>/', views.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/source/<int:source_id>/objitems/', views.SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
||||
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||
path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
|
||||
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
||||
path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
|
||||
path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||
path('lyngsat-task-status/<str:task_id>/', views.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('clear-lyngsat-cache/', views.ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
|
||||
path('', SourceListView.as_view(), name='home'),
|
||||
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
||||
path('actions/', ActionsPageView.as_view(), name='actions'),
|
||||
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||
path('satellites', AddSatellitesView.as_view(), name='add_sats'),
|
||||
path('api/locations/<int:sat_id>/geojson/', GetLocationsView.as_view(), name='locations_by_id'),
|
||||
path('transponders', AddTranspondersView.as_view(), name='add_trans'),
|
||||
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
|
||||
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
||||
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
||||
path('vch-link/', LinkVchSigmaView.as_view(), name='link_vch_sigma'),
|
||||
path('link-lyngsat/', LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
|
||||
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
||||
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
||||
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
||||
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||
path('object/<int:pk>/', ObjItemDetailView.as_view(), name='objitem_detail'),
|
||||
path('object/<int:pk>/delete/', ObjItemDeleteView.as_view(), name='objitem_delete'),
|
||||
path('fill-lyngsat-data/', FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
|
||||
path('lyngsat-task-status/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||
path('lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||
path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
|
||||
path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
|
||||
path('logout/', custom_logout, name='logout'),
|
||||
]
|
||||
75
dbapp/mainapp/views/README.md
Normal file
75
dbapp/mainapp/views/README.md
Normal 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
|
||||
72
dbapp/mainapp/views/__init__.py
Normal file
72
dbapp/mainapp/views/__init__.py
Normal 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
301
dbapp/mainapp/views/api.py
Normal 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)
|
||||
22
dbapp/mainapp/views/base.py
Normal file
22
dbapp/mainapp/views/base.py
Normal 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")
|
||||
205
dbapp/mainapp/views/data_import.py
Normal file
205
dbapp/mainapp/views/data_import.py
Normal 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")
|
||||
161
dbapp/mainapp/views/lyngsat.py
Normal file
161
dbapp/mainapp/views/lyngsat.py
Normal 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
139
dbapp/mainapp/views/map.py
Normal 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": "ок"})
|
||||
666
dbapp/mainapp/views/objitem.py
Normal file
666
dbapp/mainapp/views/objitem.py
Normal 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)
|
||||
190
dbapp/mainapp/views/source.py
Normal file
190
dbapp/mainapp/views/source.py
Normal 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)
|
||||
@@ -61,15 +61,6 @@ class AddSatellitesView(LoginRequiredMixin, View):
|
||||
return redirect("mainapp:home")
|
||||
|
||||
|
||||
# class AddTranspondersView(View):
|
||||
# def get(self, request):
|
||||
# try:
|
||||
# parse_transponders_from_json(BASE_DIR / "transponders.json")
|
||||
# except FileNotFoundError:
|
||||
# print("Файл не найден")
|
||||
# return redirect('home')
|
||||
|
||||
|
||||
class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||
template_name = "mainapp/transponders_upload.html"
|
||||
form_class = UploadFileForm
|
||||
@@ -1414,9 +1405,6 @@ class ObjItemFormView(
|
||||
geo_instance.comment = geo_form.cleaned_data["comment"]
|
||||
geo_instance.is_average = geo_form.cleaned_data["is_average"]
|
||||
|
||||
# Обрабатываем координаты
|
||||
self.process_coordinates(geo_instance)
|
||||
|
||||
# Обрабатываем дату/время
|
||||
self.process_timestamp(geo_instance)
|
||||
|
||||
Reference in New Issue
Block a user