Compare commits

...

3 Commits

27 changed files with 3527 additions and 1753 deletions

View File

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

View File

@@ -8,92 +8,121 @@
4. Иначе создать новый Source с coords_average = координаты geo_obj
"""
import os
import django
# import os
# import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dbapp.settings")
django.setup()
# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dbapp.settings")
# django.setup()
from mainapp.models import ObjItem, Source, CustomUser
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
from django.contrib.gis.db.models.functions import Distance
# from mainapp.models import ObjItem, Source, CustomUser
# from django.contrib.gis.geos import Point
# from django.contrib.gis.measure import D
# from django.contrib.gis.db.models.functions import Distance
def calculate_distance_degrees(coord1, coord2):
"""Вычисляет расстояние между двумя координатами в градусах."""
import math
# def calculate_distance_degrees(coord1, coord2):
# """Вычисляет расстояние между двумя координатами в градусах."""
# import math
# lon1, lat1 = coord1
# lon2, lat2 = coord2
# return math.sqrt((lon2 - lon1) ** 2 + (lat2 - lat1) ** 2)
# def fix_objitems_without_source():
# """Исправляет ObjItems без связи с Source."""
# # Получаем пользователя по умолчанию
# default_user = CustomUser.objects.get(id=1)
# # Получаем все ObjItems без source
# objitems_without_source = ObjItem.objects.filter(source__isnull=True)
# total_count = objitems_without_source.count()
# print(f"Найдено {total_count} ObjItems без source")
# if total_count == 0:
# print("Нечего исправлять!")
# return
# fixed_count = 0
# new_sources_count = 0
# for objitem in objitems_without_source:
# # Проверяем, есть ли geo_obj
# if not hasattr(objitem, 'geo_obj') or not objitem.geo_obj or not objitem.geo_obj.coords:
# print(f"ObjItem {objitem.id} не имеет geo_obj или координат, пропускаем")
# continue
# geo_coords = objitem.geo_obj.coords
# coord_tuple = (geo_coords.x, geo_coords.y)
# # Ищем ближайший Source
# sources_with_coords = Source.objects.filter(coords_average__isnull=False)
# closest_source = None
# min_distance = float('inf')
# for source in sources_with_coords:
# source_coord = (source.coords_average.x, source.coords_average.y)
# distance = calculate_distance_degrees(coord_tuple, source_coord)
# if distance < min_distance:
# min_distance = distance
# closest_source = source
# # Если нашли близкий Source (расстояние <= 0.5 градуса)
# if closest_source and min_distance <= 0.5:
# objitem.source = closest_source
# objitem.save()
# print(f"ObjItem {objitem.id} связан с Source {closest_source.id} (расстояние: {min_distance:.4f}°)")
# fixed_count += 1
# else:
# # Создаем новый Source
# new_source = Source.objects.create(
# coords_average=Point(coord_tuple, srid=4326),
# created_by=default_user
# )
# objitem.source = new_source
# objitem.save()
# print(f"ObjItem {objitem.id} связан с новым Source {new_source.id}")
# fixed_count += 1
# new_sources_count += 1
# print(f"\nГотово!")
# print(f"Исправлено ObjItems: {fixed_count}")
# print(f"Создано новых Source: {new_sources_count}")
# if __name__ == "__main__":
# fix_objitems_without_source()
from geographiclib.geodesic import Geodesic
def calculate_mean_coords(coord1: tuple, coord2: tuple) -> tuple[tuple, float]:
"""
Вычисляет среднюю точку между двумя координатами с использованием геодезических вычислений (с учётом эллипсоида).
:param lat1: Широта первой точки в градусах.
:param lon1: Долгота первой точки в градусах.
:param lat2: Широта второй точки в градусах.
:param lon2: Долгота второй точки в градусах.
:return: Словарь с ключами 'lat' и 'lon' для средней точки, и расстояние(dist) в КМ.
"""
lon1, lat1 = coord1
lon2, lat2 = coord2
geod_inv = Geodesic.WGS84.Inverse(lat1, lon1, lat2, lon2)
azimuth1 = geod_inv['azi1']
distance = geod_inv['s12']
geod_direct = Geodesic.WGS84.Direct(lat1, lon1, azimuth1, distance / 2)
return (geod_direct['lon2'], geod_direct['lat2']), distance/1000
return math.sqrt((lon2 - lon1) ** 2 + (lat2 - lat1) ** 2)
# Пример использования
lat1, lon1 = 56.15465080269812, 38.140518028837285
lat2, lon2 = 56.0852, 38.0852
midpoint = calculate_mean_coords((lat1, lon1), (lat2, lon2)) #56.15465080269812, 38.140518028837285
def fix_objitems_without_source():
"""Исправляет ObjItems без связи с Source."""
# Получаем пользователя по умолчанию
default_user = CustomUser.objects.get(id=1)
# Получаем все ObjItems без source
objitems_without_source = ObjItem.objects.filter(source__isnull=True)
total_count = objitems_without_source.count()
print(f"Найдено {total_count} ObjItems без source")
if total_count == 0:
print("Нечего исправлять!")
return
fixed_count = 0
new_sources_count = 0
for objitem in objitems_without_source:
# Проверяем, есть ли geo_obj
if not hasattr(objitem, 'geo_obj') or not objitem.geo_obj or not objitem.geo_obj.coords:
print(f"ObjItem {objitem.id} не имеет geo_obj или координат, пропускаем")
continue
geo_coords = objitem.geo_obj.coords
coord_tuple = (geo_coords.x, geo_coords.y)
# Ищем ближайший Source
sources_with_coords = Source.objects.filter(coords_average__isnull=False)
closest_source = None
min_distance = float('inf')
for source in sources_with_coords:
source_coord = (source.coords_average.x, source.coords_average.y)
distance = calculate_distance_degrees(coord_tuple, source_coord)
if distance < min_distance:
min_distance = distance
closest_source = source
# Если нашли близкий Source (расстояние <= 0.5 градуса)
if closest_source and min_distance <= 0.5:
objitem.source = closest_source
objitem.save()
print(f"ObjItem {objitem.id} связан с Source {closest_source.id} (расстояние: {min_distance:.4f}°)")
fixed_count += 1
else:
# Создаем новый Source
new_source = Source.objects.create(
coords_average=Point(coord_tuple, srid=4326),
created_by=default_user
)
objitem.source = new_source
objitem.save()
print(f"ObjItem {objitem.id} связан с новым Source {new_source.id}")
fixed_count += 1
new_sources_count += 1
print(f"\nГотово!")
print(f"Исправлено ObjItems: {fixed_count}")
print(f"Создано новых Source: {new_sources_count}")
if __name__ == "__main__":
fix_objitems_without_source()
print(f"Средняя точка: {midpoint[0]}")
print(f"Расстояние: {midpoint[1]} км")

View File

@@ -482,7 +482,6 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"modulation__name",
"standard__name",
)
autocomplete_fields = ("mark",)
ordering = ("-frequency",)

View File

@@ -3,6 +3,7 @@ from django import forms
# Local imports
from .models import Geo, Modulation, ObjItem, Parameter, Polarization, Satellite, Standard
from .widgets import TagSelectWidget
class UploadFileForm(forms.Form):
file = forms.FileField(
@@ -294,11 +295,21 @@ class ParameterForm(forms.ModelForm):
class GeoForm(forms.ModelForm):
class Meta:
model = Geo
fields = ['location', 'comment', 'is_average']
fields = ['location', 'comment', 'is_average', 'mirrors']
widgets = {
'location': forms.TextInput(attrs={'class': 'form-control'}),
'comment': forms.TextInput(attrs={'class': 'form-control'}),
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'mirrors': TagSelectWidget(attrs={'id': 'id_geo-mirrors'}),
}
labels = {
'location': 'Местоположение',
'comment': 'Комментарий',
'is_average': 'Усреднённое',
'mirrors': 'Спутники-зеркала, использованные для приёма',
}
help_texts = {
'mirrors': 'Начните вводить название спутника для поиска',
}
class ObjItemForm(forms.ModelForm):

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-13 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0003_source_coords_average_alter_objitem_source_and_more'),
]
operations = [
migrations.AlterField(
model_name='geo',
name='mirrors',
field=models.ManyToManyField(blank=True, help_text='Спутники-зеркала, использованные для приема', related_name='geo_mirrors', to='mainapp.satellite', verbose_name='Зеркала'),
),
]

View File

@@ -59,20 +59,16 @@ class CoordinateProcessingMixin:
Предоставляет методы для извлечения и обработки координат различных типов
(геолокация, кубсат, оперативники) из 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)
Note: Координаты Кубсата и оперативников теперь хранятся в модели Source,
а не в модели Geo, но для совместимости в форме все еще могут быть поля
для этих координат.
"""
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
"""
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
Извлекает координаты геолокации, кубсата и оперативников из POST запроса
Извлекает координаты геолокации из POST запроса
и устанавливает соответствующие поля объекта Geo.
Args:
@@ -82,28 +78,12 @@ class CoordinateProcessingMixin:
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 данных.

View File

@@ -873,11 +873,11 @@ class Geo(models.Model):
# Связи
mirrors = models.ManyToManyField(
Mirror,
Satellite,
related_name="geo_mirrors",
verbose_name="Зеркала",
blank=True,
help_text="Зеркала антенн, использованные для приема",
help_text="Спутники-зеркала, использованные для приема",
)
objitem = models.OneToOneField(
ObjItem,

View File

@@ -0,0 +1,13 @@
{% comment %}
Компонент для элемента переключения видимости столбца
Использование:
{% include 'mainapp/components/_column_toggle_item.html' with column_index=0 column_label="Выбрать" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=1 column_label="Имя" checked=True %}
{% endcomment %}
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="{{ column_index }}" {% if checked %}checked{% endif %}
onchange="toggleColumn(this)"> {{ column_label }}
</label>
</li>

View File

@@ -0,0 +1,45 @@
{% comment %}
Компонент для выпадающего списка видимости столбцов
Использование:
{% include 'mainapp/components/_column_visibility_dropdown.html' %}
{% endcomment %}
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear"></i> Колонки
</button>
<ul class="dropdown-menu" aria-labelledby="columnVisibilityDropdown" style="z-index: 1050; max-height: 300px; overflow-y: auto;">
<li>
<label class="dropdown-item">
<input type="checkbox" id="select-all-columns" unchecked
onchange="toggleAllColumns(this)"> Выбрать всё
</label>
</li>
<li>
<hr class="dropdown-divider">
</li>
{% include 'mainapp/components/_column_toggle_item.html' with column_index=0 column_label="Выбрать" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=1 column_label="Имя" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=2 column_label="Спутник" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Част, МГц" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Полоса, МГц" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Поляризация" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Сим. V" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Модул" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="ОСШ" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="Время ГЛ" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Местоположение" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Геолокация" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Обновлено" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Кем (обновление)" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Создано" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Кем (создание)" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Комментарий" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Усреднённое" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Стандарт" checked=False %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Тип источника" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Sigma" checked=True %}
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Зеркала" checked=True %}
</ul>
</div>

View File

@@ -14,7 +14,7 @@
{% if user.is_authenticated %}
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:home' %}">Объекты</a>
<a class="nav-link" href="{% url 'mainapp:objitem_list' %}">Объекты</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:actions' %}">Действия</a>

View File

@@ -47,6 +47,7 @@
<th scope="col">Кем(обн)</th>
<th scope="col">Создано</th>
<th scope="col">Кем(созд)</th>
<th scope="col">Зеркала</th>
</tr>
</thead>
<tbody id="selected-items-table-body">

View File

@@ -220,52 +220,6 @@
</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">
@@ -289,58 +243,36 @@
{% 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 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 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 class="row">
<div class="col-md-12">
<div class="mb-3">
<label class="form-label">Зеркала:</label>
<div class="readonly-field">
{% if object.geo_obj.mirrors.all %}
{% for mirror in object.geo_obj.mirrors.all %}
{{ mirror.name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
-
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<p>Нет данных о геолокации</p>
{% endif %}
</div>
<div class="d-flex justify-content-end mt-4">
@@ -368,17 +300,10 @@ document.addEventListener('DOMContentLoaded', function() {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Определяем цвета для маркеров
const colors = {
geo: 'blue',
kupsat: 'red',
valid: 'green'
};
// Функция для создания иконки маркера
function createMarkerIcon(color) {
function createMarkerIcon() {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
iconUrl: '{% static "leaflet-markers/img/marker-icon-blue.png" %}',
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
iconSize: [25, 41],
iconAnchor: [12, 41],
@@ -387,18 +312,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Маркеры
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 }};
@@ -408,66 +321,16 @@ document.addEventListener('DOMContentLoaded', function() {
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 %}
// Создаем маркер геолокации
const marker = L.marker([geoLat, geoLng], {
draggable: false,
icon: createMarkerIcon(),
title: 'Геолокация'
}).addTo(map);
marker.bindPopup('Геолокация');
{% 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);
// Центрируем карту на маркере
map.setView(marker.getLatLng(), 10);
});
</script>
{% endblock %}

View File

@@ -202,52 +202,6 @@
</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 for="id_kupsat_longitude" class="form-label">Долгота:</label>
<input type="number" step="0.000001" class="form-control"
id="id_kupsat_longitude" name="kupsat_longitude"
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|unlocalize }}{% endif %}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_kupsat_latitude" class="form-label">Широта:</label>
<input type="number" step="0.000001" class="form-control"
id="id_kupsat_latitude" name="kupsat_latitude"
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|unlocalize }}{% 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 for="id_valid_longitude" class="form-label">Долгота:</label>
<input type="number" step="0.000001" class="form-control"
id="id_valid_longitude" name="valid_longitude"
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|unlocalize }}{% endif %}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_valid_latitude" class="form-label">Широта:</label>
<input type="number" step="0.000001" class="form-control"
id="id_valid_latitude" name="valid_latitude"
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|unlocalize }}{% endif %}">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
{% include 'mainapp/components/_form_field.html' with field=geo_form.location %}
@@ -282,46 +236,11 @@
</div>
</div>
{% if object.geo_obj %}
<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 class="row">
<div class="col-md-12">
{% include 'mainapp/components/_form_field.html' with field=geo_form.mirrors %}
</div>
</div>
{% endif %}
</div>
</form>
</div>
@@ -334,7 +253,6 @@
{% leaflet_css %}
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Инициализация карты
@@ -344,17 +262,10 @@ document.addEventListener('DOMContentLoaded', function() {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Определяем цвета для маркеров
const colors = {
geo: 'blue',
kupsat: 'red',
valid: 'green'
};
// Функция для создания иконки маркера
function createMarkerIcon(color) {
function createMarkerIcon() {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
iconUrl: '{% static "leaflet-markers/img/marker-icon-blue.png" %}',
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
iconSize: [25, 41],
iconAnchor: [12, 41],
@@ -365,92 +276,54 @@ document.addEventListener('DOMContentLoaded', function() {
const editableLayerGroup = new L.FeatureGroup();
map.addLayer(editableLayerGroup);
// Маркеры
const markers = {};
function createMarker(latFieldId, lngFieldId, position, color, name) {
const marker = L.marker(position, {
draggable: false,
icon: createMarkerIcon(color),
title: name
}).addTo(editableLayerGroup);
marker.bindPopup(name);
// Маркер геолокации
const marker = L.marker([55.75, 37.62], {
draggable: false,
icon: createMarkerIcon(),
title: 'Геолокация'
}).addTo(editableLayerGroup);
marker.bindPopup('Геолокация');
// Синхронизация при изменении формы
function syncFromForm() {
const lat = parseFloat(document.getElementById(latFieldId).value);
const lng = parseFloat(document.getElementById(lngFieldId).value);
if (!isNaN(lat) && !isNaN(lng)) {
marker.setLatLng([lat, lng]);
}
// Синхронизация при изменении формы
function syncFromForm() {
const lat = parseFloat(document.getElementById('id_geo_latitude').value);
const lng = parseFloat(document.getElementById('id_geo_longitude').value);
if (!isNaN(lat) && !isNaN(lng)) {
marker.setLatLng([lat, lng]);
}
// Синхронизация при перетаскивании (только если активировано)
marker.on('dragend', function(event) {
const latLng = event.target.getLatLng();
document.getElementById(latFieldId).value = latLng.lat.toFixed(6);
document.getElementById(lngFieldId).value = latLng.lng.toFixed(6);
});
// Добавляем методы для управления
marker.enableEditing = function() {
this.dragging.enable();
this.openPopup();
};
marker.disableEditing = function() {
this.dragging.disable();
this.closePopup();
};
marker.syncFromForm = syncFromForm;
return marker;
}
// Создаем маркеры
markers.geo = createMarker(
'id_geo_latitude',
'id_geo_longitude',
[55.75, 37.62],
colors.geo,
'Геолокация'
);
// Синхронизация при перетаскивании (только если активировано)
marker.on('dragend', function(event) {
const latLng = event.target.getLatLng();
document.getElementById('id_geo_latitude').value = latLng.lat.toFixed(6);
document.getElementById('id_geo_longitude').value = latLng.lng.toFixed(6);
});
markers.kupsat = createMarker(
'id_kupsat_latitude',
'id_kupsat_longitude',
[55.75, 37.61],
colors.kupsat,
'Кубсат'
);
// Добавляем методы для управления
marker.enableEditing = function() {
this.dragging.enable();
this.openPopup();
};
markers.valid = createMarker(
'id_valid_latitude',
'id_valid_longitude',
[55.75, 37.63],
colors.valid,
'Оперативник'
);
marker.disableEditing = function() {
this.dragging.disable();
this.closePopup();
};
marker.syncFromForm = syncFromForm;
// Устанавливаем начальные координаты из полей формы
function initMarkersFromForm() {
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
const geoLng = parseFloat(document.getElementById('id_geo_longitude').value) || 37.62;
markers.geo.setLatLng([geoLat, geoLng]);
marker.setLatLng([geoLat, geoLng]);
const kupsatLat = parseFloat(document.getElementById('id_kupsat_latitude').value) || 55.75;
const kupsatLng = parseFloat(document.getElementById('id_kupsat_longitude').value) || 37.61;
markers.kupsat.setLatLng([kupsatLat, kupsatLng]);
const validLat = parseFloat(document.getElementById('id_valid_latitude').value) || 55.75;
const validLng = parseFloat(document.getElementById('id_valid_longitude').value) || 37.63;
markers.valid.setLatLng([validLat, validLng]);
// Центрируем карту на первом маркере
map.setView(markers.geo.getLatLng(), 10);
// Центрируем карту на маркере
map.setView(marker.getLatLng(), 10);
}
// Настройка формы для синхронизации с маркерами
// Настройка формы для синхронизации с маркером
function setupFormChange(latFieldId, lngFieldId, marker) {
const latField = document.getElementById(latFieldId);
const lngField = document.getElementById(lngFieldId);
@@ -470,10 +343,8 @@ document.addEventListener('DOMContentLoaded', function() {
// Инициализация
initMarkersFromForm();
// Настройка формы для синхронизации с маркерами
setupFormChange('id_geo_latitude', 'id_geo_longitude', markers.geo);
setupFormChange('id_kupsat_latitude', 'id_kupsat_longitude', markers.kupsat);
setupFormChange('id_valid_latitude', 'id_valid_longitude', markers.valid);
// Настройка формы для синхронизации с маркером
setupFormChange('id_geo_latitude', 'id_geo_longitude', marker);
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
// Кнопки редактирования
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
@@ -497,11 +368,7 @@ document.addEventListener('DOMContentLoaded', function() {
let isEditing = false;
// Сохраняем начальные координаты для отмены
const initialPositions = {
geo: markers.geo.getLatLng(),
kupsat: markers.kupsat.getLatLng(),
valid: markers.valid.getLatLng()
};
const initialPosition = marker.getLatLng();
// Включение редактирования
document.getElementById('edit-btn').addEventListener('click', function() {
@@ -512,15 +379,13 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('save-btn').disabled = false;
document.getElementById('cancel-btn').disabled = false;
// Включаем drag для всех маркеров
Object.values(markers).forEach(marker => {
marker.enableEditing();
});
// Включаем drag для маркера
marker.enableEditing();
// Показываем подсказку
L.popup()
.setLatLng(map.getCenter())
.setContent('Перетаскивайте маркеры. Нажмите "Сохранить" или "Отмена".')
.setContent('Перетаскивайте маркер. Нажмите "Сохранить" или "Отмена".')
.openOn(map);
});
@@ -534,14 +399,11 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('cancel-btn').disabled = true;
// Отключаем редактирование
Object.values(markers).forEach(marker => {
marker.disableEditing();
});
marker.disableEditing();
// Обновляем начальные позиции
initialPositions.geo = markers.geo.getLatLng();
initialPositions.kupsat = markers.kupsat.getLatLng();
initialPositions.valid = markers.valid.getLatLng();
// Обновляем начальную позицию
initialPosition.lat = marker.getLatLng().lat;
initialPosition.lng = marker.getLatLng().lng;
// Убираем попап подсказки
map.closePopup();
@@ -556,25 +418,15 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('save-btn').disabled = true;
document.getElementById('cancel-btn').disabled = true;
// Возвращаем маркеры на исходные позиции
markers.geo.setLatLng(initialPositions.geo);
markers.kupsat.setLatLng(initialPositions.kupsat);
markers.valid.setLatLng(initialPositions.valid);
// Возвращаем маркер на исходную позицию
marker.setLatLng(initialPosition);
// Отключаем редактирование
Object.values(markers).forEach(marker => {
marker.disableEditing();
});
marker.disableEditing();
// Синхронизируем форму с исходными значениями
document.getElementById('id_geo_latitude').value = initialPositions.geo.lat.toFixed(6);
document.getElementById('id_geo_longitude').value = initialPositions.geo.lng.toFixed(6);
document.getElementById('id_kupsat_latitude').value = initialPositions.kupsat.lat.toFixed(6);
document.getElementById('id_kupsat_longitude').value = initialPositions.kupsat.lng.toFixed(6);
document.getElementById('id_valid_latitude').value = initialPositions.valid.lat.toFixed(6);
document.getElementById('id_valid_longitude').value = initialPositions.valid.lng.toFixed(6);
// Синхронизируем форму с исходным значением
document.getElementById('id_geo_latitude').value = initialPosition.lat.toFixed(6);
document.getElementById('id_geo_longitude').value = initialPosition.lng.toFixed(6);
map.closePopup();
});
@@ -591,8 +443,6 @@ document.addEventListener('DOMContentLoaded', function() {
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;
};

View File

@@ -92,179 +92,7 @@
</div>
<!-- Column visibility toggle button -->
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear"></i> Колонки
</button>
<ul class="dropdown-menu" aria-labelledby="columnVisibilityDropdown" style="z-index: 1050; max-height: 300px; overflow-y: auto;">
<li>
<label class="dropdown-item">
<input type="checkbox" id="select-all-columns" unchecked
onchange="toggleAllColumns(this)"> Выбрать всё
</label>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="0" checked
onchange="toggleColumn(this)"> Выбрать
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="1" checked
onchange="toggleColumn(this)"> Имя
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="2" checked
onchange="toggleColumn(this)"> Спутник
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="3" checked
onchange="toggleColumn(this)"> Част, МГц
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="4" checked
onchange="toggleColumn(this)"> Полоса, МГц
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="5" checked
onchange="toggleColumn(this)"> Поляризация
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="6" checked
onchange="toggleColumn(this)"> Сим. V
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="7" checked
onchange="toggleColumn(this)"> Модул
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="8" checked
onchange="toggleColumn(this)"> ОСШ
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="9" checked
onchange="toggleColumn(this)"> Время ГЛ
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="10" checked
onchange="toggleColumn(this)"> Местоположение
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="11" checked
onchange="toggleColumn(this)"> Геолокация
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="12" checked
onchange="toggleColumn(this)"> Кубсат
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="13" checked
onchange="toggleColumn(this)"> Опер. отд
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="14" checked
onchange="toggleColumn(this)"> Гео-куб, км
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="15" checked
onchange="toggleColumn(this)"> Гео-опер, км
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="16" checked
onchange="toggleColumn(this)"> Куб-опер, км
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="17" checked
onchange="toggleColumn(this)"> Обновлено
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="18" checked
onchange="toggleColumn(this)"> Кем (обновление)
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="19" checked
onchange="toggleColumn(this)"> Создано
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="20" checked
onchange="toggleColumn(this)"> Кем (создание)
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="21" unchecked
onchange="toggleColumn(this)"> Комментарий
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="22" unchecked
onchange="toggleColumn(this)"> Усреднённое
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="23" unchecked
onchange="toggleColumn(this)"> Стандарт
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="24" checked
onchange="toggleColumn(this)"> Тип источника
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="25" checked
onchange="toggleColumn(this)"> Sigma
</label>
</li>
</ul>
</div>
{% include 'mainapp/components/_column_visibility_dropdown.html' %}
<!-- Pagination -->
<div class="ms-auto">
@@ -497,11 +325,6 @@
{% include 'mainapp/components/_table_header.html' with label="Время ГЛ" field="geo_timestamp" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Местоположение" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Геолокация" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Кубсат" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Опер. отд" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Гео-куб, км" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Гео-опер, км" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Куб-опер, км" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Обновлено" field="updated_at" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Кем(обн)" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Создано" field="created_at" sort=sort %}
@@ -511,6 +334,7 @@
{% include 'mainapp/components/_table_header.html' with label="Стандарт" field="standard" sort=sort %}
{% include 'mainapp/components/_table_header.html' with label="Тип источника" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Sigma" field="" sortable=False %}
{% include 'mainapp/components/_table_header.html' with label="Зеркала" field="" sortable=False %}
</tr>
</thead>
<tbody>
@@ -531,11 +355,6 @@
<td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td>
<td>{{ item.geo_location}}</td>
<td>{{ item.geo_coords }}</td>
<td>{{ item.kupsat_coords }}</td>
<td>{{ item.valid_coords }}</td>
<td>{{ item.distance_geo_kup }}</td>
<td>{{ item.distance_geo_valid }}</td>
<td>{{ item.distance_kup_valid }}</td>
<td>{{ item.obj.updated_at|date:"d.m.Y H:i" }}</td>
<td>{{ item.updated_by }}</td>
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td>
@@ -561,10 +380,11 @@
-
{% endif %}
</td>
<td>{{ item.mirrors }}</td>
</tr>
{% empty %}
<tr>
<td colspan="26" class="text-center py-4">
<td colspan="22" class="text-center py-4">
{% if selected_satellite_id %}
Нет данных для выбранных фильтров
{% else %}
@@ -900,19 +720,8 @@
// Initialize column visibility - hide creation columns by default
function initColumnVisibility() {
const creationDateCheckbox = document.querySelector('input[data-column="19"]');
const creationUserCheckbox = document.querySelector('input[data-column="20"]');
const creationDistanceGOpCheckbox = document.querySelector('input[data-column="15"]');
const creationDistanceKubOpCheckbox = document.querySelector('input[data-column="16"]');
if (creationDistanceGOpCheckbox) {
creationDistanceGOpCheckbox.checked = false;
toggleColumn(creationDistanceGOpCheckbox);
}
if (creationDistanceKubOpCheckbox) {
creationDistanceKubOpCheckbox.checked = false;
toggleColumn(creationDistanceKubOpCheckbox);
}
const creationDateCheckbox = document.querySelector('input[data-column="14"]');
const creationUserCheckbox = document.querySelector('input[data-column="15"]');
if (creationDateCheckbox) {
creationDateCheckbox.checked = false;
toggleColumn(creationDateCheckbox);
@@ -924,9 +733,9 @@
}
// Hide comment, is_average, and standard columns by default
const commentCheckbox = document.querySelector('input[data-column="21"]');
const isAverageCheckbox = document.querySelector('input[data-column="22"]');
const standardCheckbox = document.querySelector('input[data-column="23"]');
const commentCheckbox = document.querySelector('input[data-column="16"]');
const isAverageCheckbox = document.querySelector('input[data-column="17"]');
const standardCheckbox = document.querySelector('input[data-column="18"]');
if (commentCheckbox) {
commentCheckbox.checked = false;
@@ -1106,10 +915,11 @@
geo_coords: row.cells[11].textContent,
kupsat_coords: row.cells[12].textContent,
valid_coords: row.cells[13].textContent,
updated_at: row.cells[17].textContent,
updated_by: row.cells[18].textContent,
created_at: row.cells[19].textContent,
created_by: row.cells[20].textContent
updated_at: row.cells[12].textContent,
updated_by: row.cells[13].textContent,
created_at: row.cells[14].textContent,
created_by: row.cells[15].textContent,
mirrors: row.cells[21].textContent
};
window.selectedItems.push(rowData);
@@ -1171,6 +981,7 @@
<td>${item.updated_by}</td>
<td>${item.created_at}</td>
<td>${item.created_by}</td>
<td>${item.mirrors}</td>
`;
tableBody.appendChild(row);
});

View File

@@ -164,8 +164,6 @@ class CoordinateProcessingMixinTestCase(TestCase):
{
"geo_longitude": "37.62",
"geo_latitude": "55.75",
"kupsat_longitude": "37.63",
"kupsat_latitude": "55.76",
},
)
view.request = request
@@ -175,5 +173,250 @@ class CoordinateProcessingMixinTestCase(TestCase):
self.assertIsNotNone(geo_instance.coords)
self.assertEqual(geo_instance.coords.coords, (37.62, 55.75))
self.assertIsNotNone(geo_instance.coords_kupsat)
self.assertEqual(geo_instance.coords_kupsat.coords, (37.63, 55.76))
class CSVImportTestCase(TestCase):
"""Тесты для функции get_points_from_csv"""
def setUp(self):
"""Подготовка тестовых данных"""
from .models import CustomUser, Satellite, Polarization
from django.contrib.auth.models import User
# Создаем пользователя
user = User.objects.create_user(username="testuser", password="12345")
self.custom_user = CustomUser.objects.get(user=user)
# Создаем спутник
self.satellite = Satellite.objects.create(name="Test Satellite", norad=12345)
# Создаем поляризации
Polarization.objects.get_or_create(name="Вертикальная")
Polarization.objects.get_or_create(name="Горизонтальная")
Polarization.objects.get_or_create(name="Правая")
Polarization.objects.get_or_create(name="Левая")
Polarization.objects.get_or_create(name="-")
# Создаем спутники-зеркала для тестов
Satellite.objects.get_or_create(name="Mirror1 Satellite", norad=11111)
Satellite.objects.get_or_create(name="Mirror2 Satellite", norad=22222)
Satellite.objects.get_or_create(name="Mirror3 Satellite", norad=33333)
def test_initial_csv_import(self):
"""Тест первичного импорта из CSV файла"""
from .utils import get_points_from_csv
from .models import Source, ObjItem
# Тестовые данные CSV - 3 точки в разных местах
csv_content = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;Mirror3
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;
3;Signal3 V;56.8389;60.6057;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;"""
# Выполняем импорт
sources_created = get_points_from_csv(csv_content, self.custom_user)
# Проверяем результаты
# Первые две точки близко (Москва), третья далеко (Екатеринбург)
# Должно быть создано 2 источника
self.assertEqual(sources_created, 2)
self.assertEqual(Source.objects.count(), 2)
self.assertEqual(ObjItem.objects.count(), 3)
# Проверяем, что первые две точки привязаны к одному источнику
source1 = Source.objects.first()
items_in_source1 = ObjItem.objects.filter(source=source1).count()
self.assertEqual(items_in_source1, 2)
def test_csv_import_with_existing_sources(self):
"""Тест импорта CSV с существующими источниками"""
from .utils import get_points_from_csv
from .models import Source, ObjItem
# Первый импорт - создаем начальные данные
csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;"""
sources_created_1 = get_points_from_csv(csv_content_1, self.custom_user)
self.assertEqual(sources_created_1, 1)
initial_sources_count = Source.objects.count()
initial_objitems_count = ObjItem.objects.count()
# Второй импорт - добавляем новые точки
# Точка 3 - близко к существующему источнику (Москва)
# Точка 4 - далеко (Екатеринбург) - создаст новый источник
csv_content_2 = """3;Signal3 V;55.7562;37.6177;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;
4;Signal4 H;56.8389;60.6057;0;01.01.2024 12:15:00;Test Satellite;12345;11560.2;36.0;1;good;Mirror1;Mirror2;"""
sources_created_2 = get_points_from_csv(csv_content_2, self.custom_user)
# Проверяем результаты
# Должен быть создан 1 новый источник (для точки 4)
self.assertEqual(sources_created_2, 1)
self.assertEqual(Source.objects.count(), initial_sources_count + 1)
self.assertEqual(ObjItem.objects.count(), initial_objitems_count + 2)
# Проверяем, что точка 3 добавлена к существующему источнику
first_source = Source.objects.first()
items_in_first_source = ObjItem.objects.filter(source=first_source).count()
self.assertEqual(items_in_first_source, 3) # 2 начальных + 1 новая
def test_csv_import_skip_duplicates(self):
"""Тест пропуска дубликатов при импорте CSV"""
from .utils import get_points_from_csv
from .models import Source, ObjItem
# Первый импорт
csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;"""
get_points_from_csv(csv_content_1, self.custom_user)
initial_sources_count = Source.objects.count()
initial_objitems_count = ObjItem.objects.count()
# Второй импорт - та же точка (дубликат)
csv_content_2 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;"""
sources_created = get_points_from_csv(csv_content_2, self.custom_user)
# Проверяем, что дубликат пропущен
self.assertEqual(sources_created, 0)
self.assertEqual(Source.objects.count(), initial_sources_count)
self.assertEqual(ObjItem.objects.count(), initial_objitems_count)
def test_csv_import_mixed_scenario(self):
"""Тест смешанного сценария: дубликаты + новые точки + близкие точки"""
from .utils import get_points_from_csv
from .models import Source, ObjItem
# Первый импорт - 2 точки в Москве
csv_content_1 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
2;Signal2 H;55.7560;37.6175;0;01.01.2024 12:05:00;Test Satellite;12345;11520.3;36.0;1;good;Mirror1;Mirror2;"""
get_points_from_csv(csv_content_1, self.custom_user)
# Второй импорт:
# - Точка 1 (дубликат) - должна быть пропущена
# - Точка 3 (близко к Москве) - должна добавиться к существующему источнику
# - Точка 4 (Екатеринбург) - должна создать новый источник
# - Точка 5 (близко к Екатеринбургу) - должна добавиться к новому источнику
csv_content_2 = """1;Signal1 V;55.7558;37.6173;0;01.01.2024 12:00:00;Test Satellite;12345;11500.5;36.0;1;good;Mirror1;Mirror2;
3;Signal3 V;55.7562;37.6177;0;01.01.2024 12:10:00;Test Satellite;12345;11540.7;36.0;1;good;Mirror1;Mirror2;
4;Signal4 H;56.8389;60.6057;0;01.01.2024 12:15:00;Test Satellite;12345;11560.2;36.0;1;good;Mirror1;Mirror2;
5;Signal5 V;56.8391;60.6059;0;01.01.2024 12:20:00;Test Satellite;12345;11580.8;36.0;1;good;Mirror1;Mirror2;"""
sources_created = get_points_from_csv(csv_content_2, self.custom_user)
# Проверяем результаты
self.assertEqual(sources_created, 1) # Только для Екатеринбурга
self.assertEqual(Source.objects.count(), 2) # Москва + Екатеринбург
self.assertEqual(ObjItem.objects.count(), 5) # 2 начальных + 3 новых (дубликат пропущен)
# Проверяем распределение по источникам
moscow_source = Source.objects.first()
ekb_source = Source.objects.last()
moscow_items = ObjItem.objects.filter(source=moscow_source).count()
ekb_items = ObjItem.objects.filter(source=ekb_source).count()
self.assertEqual(moscow_items, 3) # 2 начальных + 1 новая
self.assertEqual(ekb_items, 2) # 2 новых точки в Екатеринбурге
class FindMirrorSatellitesTestCase(TestCase):
"""Тесты для функции find_mirror_satellites"""
def setUp(self):
"""Подготовка тестовых данных"""
from .models import Satellite
# Создаем спутники с разными именами
Satellite.objects.create(name="Eutelsat 16A", norad=40874)
Satellite.objects.create(name="Eutelsat 21B", norad=41591)
Satellite.objects.create(name="Astra 4A", norad=41404)
Satellite.objects.create(name="Turksat 4A", norad=40361)
Satellite.objects.create(name="Express AM6", norad=39508)
def test_find_exact_match(self):
"""Тест поиска спутника по точному совпадению"""
from .utils import find_mirror_satellites
mirrors = find_mirror_satellites(["Eutelsat 16A"])
self.assertEqual(len(mirrors), 1)
self.assertEqual(mirrors[0].name, "Eutelsat 16A")
def test_find_partial_match(self):
"""Тест поиска спутника по частичному совпадению"""
from .utils import find_mirror_satellites
# Ищем по части имени "Eutelsat"
mirrors = find_mirror_satellites(["eutelsat"])
self.assertEqual(len(mirrors), 2)
names = [m.name for m in mirrors]
self.assertIn("Eutelsat 16A", names)
self.assertIn("Eutelsat 21B", names)
def test_find_case_insensitive(self):
"""Тест поиска без учета регистра"""
from .utils import find_mirror_satellites
# Разные варианты регистра
mirrors1 = find_mirror_satellites(["ASTRA"])
mirrors2 = find_mirror_satellites(["astra"])
mirrors3 = find_mirror_satellites(["AsTrA"])
self.assertEqual(len(mirrors1), 1)
self.assertEqual(len(mirrors2), 1)
self.assertEqual(len(mirrors3), 1)
self.assertEqual(mirrors1[0].name, "Astra 4A")
self.assertEqual(mirrors2[0].name, "Astra 4A")
self.assertEqual(mirrors3[0].name, "Astra 4A")
def test_find_multiple_mirrors(self):
"""Тест поиска нескольких зеркал"""
from .utils import find_mirror_satellites
mirrors = find_mirror_satellites(["Eutelsat", "Turksat"])
self.assertEqual(len(mirrors), 3) # 2 Eutelsat + 1 Turksat
names = [m.name for m in mirrors]
self.assertIn("Eutelsat 16A", names)
self.assertIn("Eutelsat 21B", names)
self.assertIn("Turksat 4A", names)
def test_find_with_spaces(self):
"""Тест поиска с пробелами в начале и конце"""
from .utils import find_mirror_satellites
mirrors = find_mirror_satellites([" Express "])
self.assertEqual(len(mirrors), 1)
self.assertEqual(mirrors[0].name, "Express AM6")
def test_find_empty_list(self):
"""Тест с пустым списком"""
from .utils import find_mirror_satellites
mirrors = find_mirror_satellites([])
self.assertEqual(len(mirrors), 0)
def test_find_with_dash(self):
"""Тест с дефисом (должен быть пропущен)"""
from .utils import find_mirror_satellites
mirrors = find_mirror_satellites(["-"])
self.assertEqual(len(mirrors), 0)
def test_find_no_match(self):
"""Тест когда спутник не найден"""
from .utils import find_mirror_satellites
mirrors = find_mirror_satellites(["NonExistentSatellite"])
self.assertEqual(len(mirrors), 0)
def test_find_removes_duplicates(self):
"""Тест удаления дубликатов"""
from .utils import find_mirror_satellites
# Ищем один и тот же спутник дважды
mirrors = find_mirror_satellites(["Astra", "Astra 4A"])
self.assertEqual(len(mirrors), 1)
self.assertEqual(mirrors[0].name, "Astra 4A")

View File

@@ -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'), # Source list page
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
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'),
]

View File

@@ -10,7 +10,7 @@ from django.db.models import F
# Third-party imports
import pandas as pd
from geographiclib.geodesic import Geodesic
# Local imports
from mapsapp.models import Transponders
@@ -40,6 +40,7 @@ MAX_ITEMS_PER_PAGE = 10000
DEFAULT_NUMERIC_VALUE = -1.0
MINIMUM_BANDWIDTH_MHZ = 0.08
RANGE_DISTANCE = 56
def get_all_constants():
sats = [sat.name for sat in Satellite.objects.all()]
@@ -50,6 +51,45 @@ def get_all_constants():
return sats, standards, pols, mirrors, modulations
def find_mirror_satellites(mirror_names: list) -> list:
"""
Находит спутники, которые соответствуют именам зеркал.
Алгоритм:
1. Для каждого имени зеркала:
- Обрезать пробелы и привести к нижнему регистру
- Найти все спутники, в имени которых содержится это имя
2. Вернуть список найденных спутников
Args:
mirror_names: список имен зеркал
Returns:
list: список объектов Satellite
"""
found_satellites = []
for mirror_name in mirror_names:
if not mirror_name or mirror_name == "-":
continue
# Обрезаем пробелы и приводим к нижнему регистру
mirror_name_clean = mirror_name.strip().lower()
if not mirror_name_clean:
continue
# Ищем спутники, в имени которых содержится имя зеркала
satellites = Satellite.objects.filter(
name__icontains=mirror_name_clean
)
found_satellites.extend(satellites)
# Убираем дубликаты
return list(set(found_satellites))
def coords_transform(coords: str):
lat_part, lon_part = coords.strip().split()
sign_map = {"N": 1, "E": 1, "S": -1, "W": -1}
@@ -82,24 +122,31 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
"""
Импортирует данные из DataFrame с группировкой близких координат.
Алгоритм:
Улучшенный алгоритм с учетом существующих Source:
1. Извлечь все координаты и данные строк из DataFrame
2. Создать список необработанных записей (координата + данные строки)
3. Пока список не пуст:
3. Получить все существующие Source из БД
4. Для каждой необработанной записи:
a. Найти ближайший существующий Source (расстояние <= 56 км)
b. Если найден:
- Обновить coords_average этого Source (инкрементально)
- Создать ObjItem и связать с этим Source
- Удалить запись из списка необработанных
5. Пока список необработанных записей не пуст:
a. Взять первую запись из списка
b. Создать новый Source с coords_average = эта координата
c. Создать ObjItem для этой записи и связать с Source
d. Удалить запись из списка
e. Для каждой оставшейся записи в списке:
- Вычислить расстояние от её координаты до coords_average
- Если расстояние <= 0.5 градуса:
- Если расстояние <= 56 км:
* Вычислить новое среднее ИНКРЕМЕНТАЛЬНО:
new_avg = (coords_average + current_coord) / 2
* Обновить coords_average в Source
* Создать ObjItem для этой записи и связать с Source
* Удалить запись из списка
- Иначе: пропустить и проверить следующую запись
4. Сохранить все изменения в БД
6. Сохранить все изменения в БД
Важно: Среднее вычисляется инкрементально - каждая новая точка
усредняется с текущим средним, а не со всеми точками кластера.
@@ -136,23 +183,66 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
source_count = 0
added_to_existing_count = 0
# Шаг 3: Цикл обработки пока список не пуст
# Шаг 3: Получить все существующие Source из БД
existing_sources = list(Source.objects.filter(coords_average__isnull=False))
# Шаг 4: Попытка добавить записи к существующим Source
records_to_remove = []
for i, record in enumerate(unprocessed_records):
current_coord = record["coord"]
# Найти ближайший существующий Source
closest_source = None
min_distance = float('inf')
best_new_avg = None
for source in existing_sources:
source_coord = (source.coords_average.x, source.coords_average.y)
new_avg, distance = calculate_mean_coords(source_coord, current_coord)
if distance < min_distance:
min_distance = distance
closest_source = source
best_new_avg = new_avg
# Если найден близкий Source (расстояние <= 56 км)
if closest_source and min_distance <= RANGE_DISTANCE:
# Обновить coords_average инкрементально
closest_source.coords_average = Point(best_new_avg, srid=4326)
closest_source.save()
# Создать ObjItem и связать с существующим Source
_create_objitem_from_row(
record["row"], sat, closest_source, user_to_use, consts
)
added_to_existing_count += 1
# Пометить запись для удаления
records_to_remove.append(i)
# Удалить обработанные записи из списка (в обратном порядке, чтобы не сбить индексы)
for i in reversed(records_to_remove):
unprocessed_records.pop(i)
# Шаг 5: Цикл обработки оставшихся записей - создание новых Source
while unprocessed_records:
# Шаг 3a: Взять первую запись из списка
# Шаг 5a: Взять первую запись из списка
first_record = unprocessed_records.pop(0)
first_coord = first_record["coord"]
# Шаг 3b: Создать новый Source с coords_average = эта координата
# Шаг 5b: Создать новый Source с coords_average = эта координата
source = Source.objects.create(
coords_average=Point(first_coord, srid=4326), created_by=user_to_use
)
source_count += 1
# Шаг 3c: Создать ObjItem для этой записи и связать с Source
# Шаг 5c: Создать ObjItem для этой записи и связать с Source
_create_objitem_from_row(first_record["row"], sat, source, user_to_use, consts)
# Шаг 3e: Для каждой оставшейся записи в списке
# Шаг 5e: Для каждой оставшейся записи в списке
records_to_remove = []
for i, record in enumerate(unprocessed_records):
@@ -160,15 +250,9 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
# Вычислить расстояние от координаты до coords_average
current_avg = (source.coords_average.x, source.coords_average.y)
distance = calculate_distance_degrees(current_avg, current_coord)
# Если расстояние <= 0.5 градуса
if distance <= 0.5:
# Вычислить новое среднее ИНКРЕМЕНТАЛЬНО
new_avg = calculate_average_coords_incremental(
current_avg, current_coord
)
new_avg, distance = calculate_mean_coords(current_avg, current_coord)
if distance <= RANGE_DISTANCE:
# Обновить coords_average в Source
source.coords_average = Point(new_avg, srid=4326)
source.save()
@@ -185,6 +269,9 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
for i in reversed(records_to_remove):
unprocessed_records.pop(i)
print(f"Импорт завершен: создано {source_count} новых источников, "
f"добавлено {added_to_existing_count} точек к существующим источникам")
return source_count
@@ -230,32 +317,27 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
time_ = time(0, 0, 0)
timestamp = datetime.combine(date, time_)
# Обработка зеркал
current_mirrors = []
# Обработка зеркал - теперь это спутники
mirror_names = []
mirror_1 = row["Зеркало 1"].strip().split("\n")
mirror_2 = row["Зеркало 2"].strip().split("\n")
if len(mirror_1) > 1:
for mir in mirror_1:
Mirror.objects.get_or_create(name=mir.strip())
current_mirrors.append(mir.strip())
elif mirror_1[0] not in consts[3]:
Mirror.objects.get_or_create(name=mirror_1[0].strip())
current_mirrors.append(mirror_1[0].strip())
# Собираем все имена зеркал
for mir in mirror_1:
if mir.strip() and mir.strip() != "-":
mirror_names.append(mir.strip())
if len(mirror_2) > 1:
for mir in mirror_2:
Mirror.objects.get_or_create(name=mir.strip())
current_mirrors.append(mir.strip())
elif mirror_2[0] not in consts[3]:
Mirror.objects.get_or_create(name=mirror_2[0].strip())
current_mirrors.append(mirror_2[0].strip())
for mir in mirror_2:
if mir.strip() and mir.strip() != "-":
mirror_names.append(mir.strip())
# Находим спутники-зеркала
mirror_satellites = find_mirror_satellites(mirror_names)
location = row["Местоопределение"].strip()
comment = row["Комментарий"]
source_name = row["Объект наблюдения"]
# Создаем Geo объект (БЕЗ coords_kupsat и coords_valid)
geo, _ = Geo.objects.get_or_create(
timestamp=timestamp,
coords=geo_point,
@@ -266,7 +348,10 @@ def _create_objitem_from_row(row, sat, source, user_to_use, consts):
},
)
geo.save()
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
# Устанавливаем связи с спутниками-зеркалами
if mirror_satellites:
geo.mirrors.set(mirror_satellites)
# Проверяем, существует ли уже ObjItem с таким же geo
existing_obj_item = ObjItem.objects.filter(geo_obj=geo).first()
@@ -386,7 +471,7 @@ def get_points_from_csv(file_content, current_user=None):
4. Для каждой записи:
a. Проверить, существует ли дубликат (координаты + частота)
b. Если дубликат найден, пропустить запись
c. Найти ближайший существующий Source (расстояние <= 0.5 градуса)
c. Найти ближайший существующий Source (расстояние <= 56 км)
d. Если найден:
- Обновить coords_average этого Source (инкрементально)
- Создать ObjItem и связать с этим Source
@@ -466,21 +551,21 @@ def get_points_from_csv(file_content, current_user=None):
# Шаг 4c: Найти ближайший существующий Source
closest_source = None
min_distance = float('inf')
best_new_avg = None
for source in existing_sources:
source_coord = (source.coords_average.x, source.coords_average.y)
distance = calculate_distance_degrees(source_coord, current_coord)
new_avg, distance = calculate_mean_coords(source_coord, current_coord)
if distance < min_distance:
min_distance = distance
closest_source = source
best_new_avg = new_avg
# Шаг 4d: Если найден близкий Source (расстояние <= 0.5 градуса)
if closest_source and min_distance <= 0.5:
# Шаг 4d: Если найден близкий Source (расстояние <= 56 км)
if closest_source and min_distance <= RANGE_DISTANCE:
# Обновить coords_average инкрементально
current_avg = (closest_source.coords_average.x, closest_source.coords_average.y)
new_avg = calculate_average_coords_incremental(current_avg, current_coord)
closest_source.coords_average = Point(new_avg, srid=4326)
closest_source.coords_average = Point(best_new_avg, srid=4326)
closest_source.save()
# Создать ObjItem и связать с существующим Source
@@ -507,7 +592,7 @@ def get_points_from_csv(file_content, current_user=None):
return new_sources_count
def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.001):
def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.1):
"""
Проверяет, существует ли уже ObjItem с такими же координатами и частотой.
@@ -515,7 +600,7 @@ def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.001):
coord_tuple: кортеж (lon, lat) координат
frequency: частота в МГц
freq_range: полоса частот в МГц
tolerance: допуск для сравнения координат в градусах (по умолчанию 0.001 ≈ 100м)
tolerance: допуск для сравнения координат в километрах
Returns:
bool: True если дубликат найден, False иначе
@@ -531,7 +616,7 @@ def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.001):
# Проверяем расстояние между координатами
geo_coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
distance = calculate_distance_degrees(coord_tuple, geo_coord)
_, distance = calculate_mean_coords(coord_tuple, geo_coord)
if distance <= tolerance:
# Координаты совпадают, проверяем частоту
@@ -571,14 +656,20 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
sat_obj, _ = Satellite.objects.get_or_create(
name=row["sat"], defaults={"norad": row["norad_id"]}
)
mir_1_obj, _ = Mirror.objects.get_or_create(name=row["mir_1"])
mir_2_obj, _ = Mirror.objects.get_or_create(name=row["mir_2"])
mir_lst = [row["mir_1"], row["mir_2"]]
if not pd.isna(row["mir_3"]):
mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
mir_lst.append(row["mir_3"])
# Создаем Geo объект (БЕЗ coords_kupsat и coords_valid)
# Обработка зеркал - теперь это спутники
mirror_names = []
if not pd.isna(row["mir_1"]) and row["mir_1"].strip() != "-":
mirror_names.append(row["mir_1"])
if not pd.isna(row["mir_2"]) and row["mir_2"].strip() != "-":
mirror_names.append(row["mir_2"])
if not pd.isna(row["mir_3"]) and row["mir_3"].strip() != "-":
mirror_names.append(row["mir_3"])
# Находим спутники-зеркала
mirror_satellites = find_mirror_satellites(mirror_names)
# Создаем Geo объект
geo_obj, _ = Geo.objects.get_or_create(
timestamp=row["time"],
coords=Point(row["lon"], row["lat"], srid=4326),
@@ -586,7 +677,10 @@ def _create_objitem_from_csv_row(row, source, user_to_use):
"is_average": False,
},
)
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
# Устанавливаем связи с спутниками-зеркалами
if mirror_satellites:
geo_obj.mirrors.set(mirror_satellites)
# Проверяем, существует ли уже ObjItem с таким же geo
existing_obj_item = ObjItem.objects.filter(geo_obj=geo_obj).first()
@@ -873,40 +967,24 @@ def kub_report(data_in: io.StringIO) -> pd.DataFrame:
# ============================================================================
def calculate_distance_degrees(coord1: tuple, coord2: tuple) -> float:
def calculate_mean_coords(coord1: tuple, coord2: tuple) -> tuple[tuple, float]:
"""
Вычисляет расстояние между двумя координатами в градусах.
Вычисляет среднюю точку между двумя координатами с использованием геодезических вычислений (с учётом эллипсоида).
Использует простую евклидову метрику для малых расстояний.
Подходит для определения близости точек в радиусе до нескольких градусов.
Args:
coord1 (tuple): Первая координата в формате (longitude, latitude)
coord2 (tuple): Вторая координата в формате (longitude, latitude)
Returns:
float: Расстояние в градусах
Example:
>>> dist = calculate_distance_degrees((37.62, 55.75), (37.63, 55.76))
>>> print(f"{dist:.4f}") # ~0.0141 градусов
0.0141
>>> dist = calculate_distance_degrees((37.62, 55.75), (37.62, 55.75))
>>> print(dist) # Одинаковые координаты
0.0
:param lat1: Широта первой точки в градусах.
:param lon1: Долгота первой точки в градусах.
:param lat2: Широта второй точки в градусах.
:param lon2: Долгота второй точки в градусах.
:return: Словарь с ключами 'lat' и 'lon' для средней точки, и расстояние(dist) в КМ.
"""
lon1, lat1 = coord1
lon2, lat2 = coord2
geod_inv = Geodesic.WGS84.Inverse(lat1, lon1, lat2, lon2)
azimuth1 = geod_inv['azi1']
distance = geod_inv['s12']
geod_direct = Geodesic.WGS84.Direct(lat1, lon1, azimuth1, distance / 2)
return (geod_direct['lon2'], geod_direct['lat2']), distance/1000
# Простая евклидова метрика для малых расстояний
# Для более точных расчетов на больших расстояниях можно использовать формулу гаверсинуса
delta_lon = lon2 - lon1
delta_lat = lat2 - lat1
distance = (delta_lon**2 + delta_lat**2) ** 0.5
return distance
def calculate_average_coords_incremental(

View File

@@ -0,0 +1,75 @@
# Views Module Structure
This directory contains the refactored views from the original monolithic `views.py` file.
## File Organization
### `__init__.py`
Central import file that exports all views for easy access. This allows other modules to import views using:
```python
from mainapp.views import ObjItemListView, custom_logout
```
### `base.py`
Basic views and utilities:
- `ActionsPageView` - Displays the actions page
- `custom_logout()` - Custom logout function
### `objitem.py`
ObjItem CRUD operations and related views:
- `ObjItemListView` - List view with filtering and pagination
- `ObjItemFormView` - Base class for create/update operations
- `ObjItemCreateView` - Create new ObjItem
- `ObjItemUpdateView` - Update existing ObjItem
- `ObjItemDeleteView` - Delete ObjItem
- `ObjItemDetailView` - Read-only detail view
- `DeleteSelectedObjectsView` - Bulk delete operation
### `data_import.py`
Data import views for various formats:
- `AddSatellitesView` - Add satellites to database
- `AddTranspondersView` - Upload and parse transponder data from XML
- `LoadExcelDataView` - Load data from Excel files
- `LoadCsvDataView` - Load data from CSV files
- `UploadVchLoadView` - Upload VCH load data from HTML
- `LinkVchSigmaView` - Link VCH data with Sigma parameters
- `ProcessKubsatView` - Process Kubsat event data
### `api.py`
API endpoints for AJAX requests:
- `GetLocationsView` - Get locations by satellite ID in GeoJSON format
- `LyngsatDataAPIView` - Get LyngSat source data
- `SigmaParameterDataAPIView` - Get SigmaParameter data
- `SourceObjItemsAPIView` - Get ObjItems related to a Source
- `LyngsatTaskStatusAPIView` - Get Celery task status
### `lyngsat.py`
LyngSat related views:
- `LinkLyngsatSourcesView` - Link LyngSat sources to objects
- `FillLyngsatDataView` - Fill data from Lyngsat website
- `LyngsatTaskStatusView` - Track Lyngsat data filling task status
- `ClearLyngsatCacheView` - Clear LyngSat cache
### `source.py`
Source related views:
- `SourceListView` - List view for Source objects with filtering
### `map.py`
Map related views:
- `ShowMapView` - Display objects on map (admin interface)
- `ShowSelectedObjectsMapView` - Display selected objects on map
- `ClusterTestView` - Test view for clustering functionality
## Migration Notes
The original `views.py` has been renamed to `views_old.py` as a backup. All imports have been updated in:
- `dbapp/mainapp/urls.py`
- `dbapp/dbapp/urls.py`
## Benefits of This Structure
1. **Better Organization** - Related views are grouped together
2. **Easier Maintenance** - Smaller files are easier to navigate and modify
3. **Clear Responsibilities** - Each file has a specific purpose
4. **Improved Testability** - Easier to write focused unit tests
5. **Better Collaboration** - Multiple developers can work on different files without conflicts

View File

@@ -0,0 +1,72 @@
# Import all views for easy access
from .base import ActionsPageView, custom_logout
from .objitem import (
ObjItemListView,
ObjItemCreateView,
ObjItemUpdateView,
ObjItemDeleteView,
ObjItemDetailView,
DeleteSelectedObjectsView,
)
from .data_import import (
AddSatellitesView,
AddTranspondersView,
LoadExcelDataView,
LoadCsvDataView,
UploadVchLoadView,
LinkVchSigmaView,
ProcessKubsatView,
)
from .api import (
GetLocationsView,
LyngsatDataAPIView,
SigmaParameterDataAPIView,
SourceObjItemsAPIView,
LyngsatTaskStatusAPIView,
)
from .lyngsat import (
LinkLyngsatSourcesView,
FillLyngsatDataView,
LyngsatTaskStatusView,
ClearLyngsatCacheView,
)
from .source import SourceListView
from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView
__all__ = [
# Base
'ActionsPageView',
'custom_logout',
# ObjItem
'ObjItemListView',
'ObjItemCreateView',
'ObjItemUpdateView',
'ObjItemDeleteView',
'ObjItemDetailView',
'DeleteSelectedObjectsView',
# Data Import
'AddSatellitesView',
'AddTranspondersView',
'LoadExcelDataView',
'LoadCsvDataView',
'UploadVchLoadView',
'LinkVchSigmaView',
'ProcessKubsatView',
# API
'GetLocationsView',
'LyngsatDataAPIView',
'SigmaParameterDataAPIView',
'SourceObjItemsAPIView',
'LyngsatTaskStatusAPIView',
# LyngSat
'LinkLyngsatSourcesView',
'FillLyngsatDataView',
'LyngsatTaskStatusView',
'ClearLyngsatCacheView',
# Source
'SourceListView',
# Map
'ShowMapView',
'ShowSelectedObjectsMapView',
'ClusterTestView',
]

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

@@ -0,0 +1,301 @@
"""
API endpoints for AJAX requests and data retrieval.
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from django.utils import timezone
from django.views import View
from ..models import ObjItem
class GetLocationsView(LoginRequiredMixin, View):
"""API endpoint for getting locations by satellite ID in GeoJSON format."""
def get(self, request, sat_id):
locations = (
ObjItem.objects.filter(parameter_obj__id_satellite=sat_id)
.select_related(
"geo_obj",
"parameter_obj",
"parameter_obj__polarization",
)
)
if not locations.exists():
return JsonResponse({"error": "Объектов не найдено"}, status=404)
features = []
for loc in locations:
if not hasattr(loc, "geo_obj") or not loc.geo_obj or not loc.geo_obj.coords:
continue
param = getattr(loc, 'parameter_obj', None)
if not param:
continue
features.append(
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [loc.geo_obj.coords[0], loc.geo_obj.coords[1]],
},
"properties": {
"pol": param.polarization.name if param.polarization else "-",
"freq": param.frequency * 1000000 if param.frequency else 0,
"name": loc.name or "-",
"id": loc.geo_obj.id,
},
}
)
return JsonResponse({"type": "FeatureCollection", "features": features})
class LyngsatDataAPIView(LoginRequiredMixin, View):
"""API endpoint for getting LyngSat source data."""
def get(self, request, lyngsat_id):
from lyngsatapp.models import LyngSat
try:
lyngsat = LyngSat.objects.select_related(
'id_satellite',
'polarization',
'modulation',
'standard'
).get(id=lyngsat_id)
# Format date with local timezone
last_update_str = '-'
if lyngsat.last_update:
local_time = timezone.localtime(lyngsat.last_update)
last_update_str = local_time.strftime("%d.%m.%Y")
data = {
'id': lyngsat.id,
'satellite': lyngsat.id_satellite.name if lyngsat.id_satellite else '-',
'frequency': f"{lyngsat.frequency:.3f}" if lyngsat.frequency else '-',
'polarization': lyngsat.polarization.name if lyngsat.polarization else '-',
'modulation': lyngsat.modulation.name if lyngsat.modulation else '-',
'standard': lyngsat.standard.name if lyngsat.standard else '-',
'sym_velocity': f"{lyngsat.sym_velocity:.0f}" if lyngsat.sym_velocity else '-',
'fec': lyngsat.fec or '-',
'channel_info': lyngsat.channel_info or '-',
'last_update': last_update_str,
'url': lyngsat.url or None,
}
return JsonResponse(data)
except LyngSat.DoesNotExist:
return JsonResponse({'error': 'Источник LyngSat не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
class SigmaParameterDataAPIView(LoginRequiredMixin, View):
"""API endpoint for getting SigmaParameter data."""
def get(self, request, parameter_id):
from ..models import Parameter
try:
parameter = Parameter.objects.select_related(
'id_satellite',
'polarization',
'modulation',
'standard'
).prefetch_related(
'sigma_parameter__mark',
'sigma_parameter__id_satellite',
'sigma_parameter__polarization',
'sigma_parameter__modulation',
'sigma_parameter__standard'
).get(id=parameter_id)
# Get all related SigmaParameter
sigma_params = parameter.sigma_parameter.all()
sigma_data = []
for sigma in sigma_params:
# Get marks
marks = []
for mark in sigma.mark.all().order_by('-timestamp'):
mark_str = '+' if mark.mark else '-'
date_str = '-'
if mark.timestamp:
local_time = timezone.localtime(mark.timestamp)
date_str = local_time.strftime("%d.%m.%Y %H:%M")
marks.append({
'mark': mark_str,
'date': date_str
})
# Format start and end dates
datetime_begin_str = '-'
if sigma.datetime_begin:
local_time = timezone.localtime(sigma.datetime_begin)
datetime_begin_str = local_time.strftime("%d.%m.%Y %H:%M")
datetime_end_str = '-'
if sigma.datetime_end:
local_time = timezone.localtime(sigma.datetime_end)
datetime_end_str = local_time.strftime("%d.%m.%Y %H:%M")
sigma_data.append({
'id': sigma.id,
'satellite': sigma.id_satellite.name if sigma.id_satellite else '-',
'frequency': f"{sigma.frequency:.3f}" if sigma.frequency else '-',
'transfer_frequency': f"{sigma.transfer_frequency:.3f}" if sigma.transfer_frequency else '-',
'freq_range': f"{sigma.freq_range:.3f}" if sigma.freq_range else '-',
'polarization': sigma.polarization.name if sigma.polarization else '-',
'modulation': sigma.modulation.name if sigma.modulation else '-',
'standard': sigma.standard.name if sigma.standard else '-',
'bod_velocity': f"{sigma.bod_velocity:.0f}" if sigma.bod_velocity else '-',
'snr': f"{sigma.snr:.1f}" if sigma.snr is not None else '-',
'power': f"{sigma.power:.1f}" if sigma.power is not None else '-',
'status': sigma.status or '-',
'packets': 'Да' if sigma.packets else 'Нет' if sigma.packets is not None else '-',
'datetime_begin': datetime_begin_str,
'datetime_end': datetime_end_str,
'marks': marks
})
return JsonResponse({
'parameter_id': parameter.id,
'sigma_parameters': sigma_data
})
except Parameter.DoesNotExist:
return JsonResponse({'error': 'Parameter не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
class SourceObjItemsAPIView(LoginRequiredMixin, View):
"""API endpoint for getting ObjItems related to a Source."""
def get(self, request, source_id):
from ..models import Source
try:
# Load Source with prefetch_related for ObjItem
source = Source.objects.prefetch_related(
'source_objitems',
'source_objitems__parameter_obj',
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
'source_objitems__geo_obj'
).get(id=source_id)
# Get all related ObjItems, sorted by created_at
objitems = source.source_objitems.all().order_by('created_at')
objitems_data = []
for objitem in objitems:
# Get parameter data
param = getattr(objitem, 'parameter_obj', None)
satellite_name = '-'
frequency = '-'
freq_range = '-'
polarization = '-'
bod_velocity = '-'
modulation = '-'
snr = '-'
if param:
if hasattr(param, 'id_satellite') and param.id_satellite:
satellite_name = param.id_satellite.name
frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-'
freq_range = f"{param.freq_range:.3f}" if param.freq_range is not None else '-'
if hasattr(param, 'polarization') and param.polarization:
polarization = param.polarization.name
bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else '-'
if hasattr(param, 'modulation') and param.modulation:
modulation = param.modulation.name
snr = f"{param.snr:.0f}" if param.snr is not None else '-'
# Get geo data
geo_timestamp = '-'
geo_location = '-'
geo_coords = '-'
if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
if objitem.geo_obj.timestamp:
local_time = timezone.localtime(objitem.geo_obj.timestamp)
geo_timestamp = local_time.strftime("%d.%m.%Y %H:%M")
geo_location = objitem.geo_obj.location or '-'
if objitem.geo_obj.coords:
longitude = objitem.geo_obj.coords.coords[0]
latitude = objitem.geo_obj.coords.coords[1]
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
geo_coords = f"{lat} {lon}"
objitems_data.append({
'id': objitem.id,
'name': objitem.name or '-',
'satellite_name': satellite_name,
'frequency': frequency,
'freq_range': freq_range,
'polarization': polarization,
'bod_velocity': bod_velocity,
'modulation': modulation,
'snr': snr,
'geo_timestamp': geo_timestamp,
'geo_location': geo_location,
'geo_coords': geo_coords
})
return JsonResponse({
'source_id': source_id,
'objitems': objitems_data
})
except Source.DoesNotExist:
return JsonResponse({'error': 'Источник не найден'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
"""API endpoint for getting Celery task status."""
def get(self, request, task_id):
from celery.result import AsyncResult
from django.core.cache import cache
task = AsyncResult(task_id)
response_data = {
'task_id': task_id,
'state': task.state,
'result': None,
'error': None
}
if task.state == 'PENDING':
response_data['status'] = 'Задача в очереди...'
elif task.state == 'PROGRESS':
response_data['status'] = task.info.get('status', '')
response_data['current'] = task.info.get('current', 0)
response_data['total'] = task.info.get('total', 1)
response_data['percent'] = int((task.info.get('current', 0) / task.info.get('total', 1)) * 100)
elif task.state == 'SUCCESS':
# Get result from cache
result = cache.get(f'lyngsat_task_{task_id}')
if result:
response_data['result'] = result
response_data['status'] = 'Задача завершена успешно'
else:
response_data['result'] = task.result
response_data['status'] = 'Задача завершена'
elif task.state == 'FAILURE':
response_data['status'] = 'Ошибка при выполнении задачи'
response_data['error'] = str(task.info)
else:
response_data['status'] = task.state
return JsonResponse(response_data)

View File

@@ -0,0 +1,22 @@
"""
Base views and utilities.
"""
from django.contrib.auth import logout
from django.shortcuts import redirect, render
from django.views import View
class ActionsPageView(View):
"""View for displaying the actions page."""
def get(self, request):
if request.user.is_authenticated:
return render(request, "mainapp/actions.html")
else:
return render(request, "mainapp/login_required.html")
def custom_logout(request):
"""Custom logout view."""
logout(request)
return redirect("mainapp:home")

View File

@@ -0,0 +1,205 @@
"""
Data import views (Excel, CSV, Transponders, VCH, etc.).
"""
from io import BytesIO
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views import View
from django.views.generic import FormView
import pandas as pd
from ..forms import (
LoadCsvData,
LoadExcelData,
NewEventForm,
UploadFileForm,
UploadVchLoad,
VchLinkForm,
)
from ..mixins import FormMessageMixin
from ..utils import (
add_satellite_list,
compare_and_link_vch_load,
fill_data_from_df,
get_points_from_csv,
get_vch_load_from_html,
kub_report,
)
from mapsapp.utils import parse_transponders_from_xml
class AddSatellitesView(LoginRequiredMixin, View):
"""View for adding satellites to the database."""
def get(self, request):
add_satellite_list()
return redirect("mainapp:home")
class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
"""View for uploading and parsing transponder data from XML."""
template_name = "mainapp/transponders_upload.html"
form_class = UploadFileForm
success_message = "Файл успешно обработан"
error_message = "Форма заполнена некорректно"
def form_valid(self, form):
uploaded_file = self.request.FILES["file"]
try:
content = uploaded_file.read()
parse_transponders_from_xml(BytesIO(content))
except ValueError as e:
messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
return redirect("mainapp:add_trans")
except Exception as e:
messages.error(self.request, f"Неизвестная ошибка: {e}")
return redirect("mainapp:add_trans")
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("mainapp:add_trans")
class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
"""View for loading data from Excel files."""
template_name = "mainapp/add_data_from_excel.html"
form_class = LoadExcelData
error_message = "Форма заполнена некорректно"
def form_valid(self, form):
uploaded_file = self.request.FILES["file"]
selected_sat = form.cleaned_data["sat_choice"]
number = form.cleaned_data["number_input"]
try:
import io
df = pd.read_excel(io.BytesIO(uploaded_file.read()))
if number > 0:
df = df.head(number)
result = fill_data_from_df(df, selected_sat, self.request.user.customuser)
messages.success(
self.request, f"Данные успешно загружены! Обработано строк: {result}"
)
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
return redirect("mainapp:load_excel_data")
def get_success_url(self):
return reverse_lazy("mainapp:load_excel_data")
class LoadCsvDataView(LoginRequiredMixin, FormMessageMixin, FormView):
"""View for loading data from CSV files."""
template_name = "mainapp/add_data_from_csv.html"
form_class = LoadCsvData
success_message = "Данные успешно загружены!"
error_message = "Форма заполнена некорректно"
def form_valid(self, form):
uploaded_file = self.request.FILES["file"]
try:
content = uploaded_file.read()
if isinstance(content, bytes):
content = content.decode("utf-8")
get_points_from_csv(content, self.request.user.customuser)
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
return redirect("mainapp:load_csv_data")
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("mainapp:load_csv_data")
class UploadVchLoadView(LoginRequiredMixin, FormMessageMixin, FormView):
"""View for uploading VCH load data from HTML files."""
template_name = "mainapp/upload_html.html"
form_class = UploadVchLoad
success_message = "Файл успешно обработан"
error_message = "Форма заполнена некорректно"
def form_valid(self, form):
selected_sat = form.cleaned_data["sat_choice"]
uploaded_file = self.request.FILES["file"]
try:
get_vch_load_from_html(uploaded_file, selected_sat)
except ValueError as e:
messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
return redirect("mainapp:vch_load")
except Exception as e:
messages.error(self.request, f"Неизвестная ошибка: {e}")
return redirect("mainapp:vch_load")
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("mainapp:vch_load")
class LinkVchSigmaView(LoginRequiredMixin, FormView):
"""View for linking VCH data with Sigma parameters."""
template_name = "mainapp/link_vch.html"
form_class = VchLinkForm
def form_valid(self, form):
# value1 is no longer used - frequency tolerance is determined automatically
freq_range = form.cleaned_data["value2"]
sat_id = form.cleaned_data["sat_choice"]
# Pass 0 for eps_freq and ku_range as they are not used
count_all, link_count = compare_and_link_vch_load(sat_id, 0, freq_range, 0)
messages.success(
self.request, f"Привязано {link_count} из {count_all} объектов"
)
return redirect("mainapp:link_vch_sigma")
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
"""View for processing Kubsat event data."""
template_name = "mainapp/process_kubsat.html"
form_class = NewEventForm
error_message = "Форма заполнена некорректно"
def form_valid(self, form):
uploaded_file = self.request.FILES["file"]
try:
content = uploaded_file.read()
df = kub_report(BytesIO(content))
output = BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer:
df.to_excel(writer, index=False, sheet_name="Результат")
output.seek(0)
response = HttpResponse(
output.getvalue(),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
response["Content-Disposition"] = (
'attachment; filename="kubsat_report.xlsx"'
)
messages.success(self.request, "Событие успешно обработано!")
return response
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
return redirect("mainapp:kubsat_excel")

View File

@@ -0,0 +1,161 @@
"""
LyngSat related views for data synchronization and linking.
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, render
from django.urls import reverse_lazy
from django.views import View
from django.views.generic import FormView
from ..forms import FillLyngsatDataForm, LinkLyngsatForm
from ..mixins import FormMessageMixin
from ..models import ObjItem
class LinkLyngsatSourcesView(LoginRequiredMixin, FormMessageMixin, FormView):
"""View for linking LyngSat sources to objects."""
template_name = "mainapp/link_lyngsat.html"
form_class = LinkLyngsatForm
success_message = "Привязка источников LyngSat завершена"
error_message = "Ошибка при привязке источников"
def form_valid(self, form):
from lyngsatapp.models import LyngSat
satellites = form.cleaned_data.get("satellites")
frequency_tolerance = form.cleaned_data.get("frequency_tolerance", 0.5)
# If satellites not selected, process all
if satellites:
objitems = ObjItem.objects.filter(
parameter_obj__id_satellite__in=satellites
).select_related('parameter_obj', 'parameter_obj__polarization')
else:
objitems = ObjItem.objects.filter(
parameter_obj__isnull=False
).select_related('parameter_obj', 'parameter_obj__polarization')
linked_count = 0
total_count = objitems.count()
for objitem in objitems:
if not hasattr(objitem, 'parameter_obj') or not objitem.parameter_obj:
continue
param = objitem.parameter_obj
# Round object frequency
if param.frequency:
rounded_freq = round(param.frequency, 0) # Round to integer
# Find matching LyngSat source
# Compare by rounded frequency and polarization
lyngsat_sources = LyngSat.objects.filter(
id_satellite=param.id_satellite,
polarization=param.polarization,
frequency__gte=rounded_freq - frequency_tolerance,
frequency__lte=rounded_freq + frequency_tolerance
).order_by('frequency')
if lyngsat_sources.exists():
# Take first matching source
objitem.lyngsat_source = lyngsat_sources.first()
objitem.save(update_fields=['lyngsat_source'])
linked_count += 1
messages.success(
self.request,
f"Привязано {linked_count} из {total_count} объектов к источникам LyngSat"
)
return redirect("mainapp:link_lyngsat")
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
class FillLyngsatDataView(LoginRequiredMixin, FormMessageMixin, FormView):
"""
View for filling data from Lyngsat.
Allows selecting satellites and regions for parsing data from Lyngsat website.
Starts asynchronous Celery task for processing.
"""
template_name = "mainapp/fill_lyngsat_data.html"
form_class = FillLyngsatDataForm
success_url = reverse_lazy("mainapp:lyngsat_task_status")
error_message = "Форма заполнена некорректно"
def form_valid(self, form):
satellites = form.cleaned_data["satellites"]
regions = form.cleaned_data["regions"]
use_cache = form.cleaned_data.get("use_cache", True)
force_refresh = form.cleaned_data.get("force_refresh", False)
# Get satellite names
target_sats = [sat.name for sat in satellites]
try:
from lyngsatapp.tasks import fill_lyngsat_data_task
# Start asynchronous task with caching parameters
task = fill_lyngsat_data_task.delay(
target_sats,
regions,
force_refresh=force_refresh,
use_cache=use_cache
)
cache_status = "без кеша" if not use_cache else ("с обновлением кеша" if force_refresh else "с кешированием")
messages.success(
self.request,
f"Задача запущена ({cache_status})! ID задачи: {task.id}. "
"Вы будете перенаправлены на страницу отслеживания прогресса."
)
# Redirect to task status page
return redirect('mainapp:lyngsat_task_status', task_id=task.id)
except Exception as e:
messages.error(self.request, f"Ошибка при запуске задачи: {str(e)}")
return redirect("mainapp:fill_lyngsat_data")
class LyngsatTaskStatusView(LoginRequiredMixin, View):
"""View for tracking Lyngsat data filling task status."""
template_name = "mainapp/lyngsat_task_status.html"
def get(self, request, task_id=None):
context = {
'task_id': task_id
}
return render(request, self.template_name, context)
class ClearLyngsatCacheView(LoginRequiredMixin, View):
"""View for clearing LyngSat cache."""
def post(self, request):
from lyngsatapp.tasks import clear_cache_task
cache_type = request.POST.get('cache_type', 'all')
try:
# Start cache clearing task
task = clear_cache_task.delay(cache_type)
messages.success(
request,
f"Задача очистки кеша ({cache_type}) запущена! ID задачи: {task.id}"
)
except Exception as e:
messages.error(request, f"Ошибка при запуске задачи очистки кеша: {str(e)}")
return redirect(request.META.get('HTTP_REFERER', 'mainapp:home'))
def get(self, request):
"""Cache management page."""
return render(request, 'mainapp/clear_lyngsat_cache.html')

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

@@ -0,0 +1,139 @@
"""
Map related views for displaying objects on maps.
"""
from collections import defaultdict
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.views import View
from ..clusters import get_clusters
from ..mixins import RoleRequiredMixin
from ..models import ObjItem
@method_decorator(staff_member_required, name="dispatch")
class ShowMapView(RoleRequiredMixin, View):
"""View for displaying objects on map (admin interface)."""
required_roles = ["admin", "moderator"]
def get(self, request):
ids = request.GET.get("ids", "")
points = []
if ids:
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
locations = ObjItem.objects.filter(id__in=id_list).select_related(
"parameter_obj",
"parameter_obj__id_satellite",
"parameter_obj__polarization",
"parameter_obj__modulation",
"parameter_obj__standard",
"geo_obj",
)
for obj in locations:
if (
not hasattr(obj, "geo_obj")
or not obj.geo_obj
or not obj.geo_obj.coords
):
continue
param = getattr(obj, 'parameter_obj', None)
if not param:
continue
points.append(
{
"name": f"{obj.name}",
"freq": f"{param.frequency} [{param.freq_range}] МГц",
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
}
)
else:
return redirect("admin")
grouped = defaultdict(list)
for p in points:
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
groups = [
{"name": name, "points": coords_list}
for name, coords_list in grouped.items()
]
context = {
"groups": groups,
}
return render(request, "admin/map_custom.html", context)
class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
"""View for displaying selected objects on map."""
def get(self, request):
ids = request.GET.get("ids", "")
points = []
if ids:
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
locations = ObjItem.objects.filter(id__in=id_list).select_related(
"parameter_obj",
"parameter_obj__id_satellite",
"parameter_obj__polarization",
"parameter_obj__modulation",
"parameter_obj__standard",
"geo_obj",
)
for obj in locations:
if (
not hasattr(obj, "geo_obj")
or not obj.geo_obj
or not obj.geo_obj.coords
):
continue
param = getattr(obj, 'parameter_obj', None)
if not param:
continue
points.append(
{
"name": f"{obj.name}",
"freq": f"{param.frequency} [{param.freq_range}] МГц",
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
}
)
else:
return redirect("mainapp:objitem_list")
# Group points by object name
grouped = defaultdict(list)
for p in points:
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
groups = [
{"name": name, "points": coords_list}
for name, coords_list in grouped.items()
]
context = {
"groups": groups,
}
return render(request, "mainapp/objitem_map.html", context)
class ClusterTestView(LoginRequiredMixin, View):
"""Test view for clustering functionality."""
def get(self, request):
objs = ObjItem.objects.filter(
name__icontains="! Astra 4A 12654,040 [1,962] МГц H"
)
coords = []
for obj in objs:
if hasattr(obj, "geo_obj") and obj.geo_obj and obj.geo_obj.coords:
coords.append(
(obj.geo_obj.coords.coords[1], obj.geo_obj.coords.coords[0])
)
get_clusters(coords)
return JsonResponse({"success": "ок"})

View File

@@ -0,0 +1,675 @@
"""
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 = "-"
mirrors_list = []
if hasattr(obj, "geo_obj") and obj.geo_obj:
geo_timestamp = obj.geo_obj.timestamp
geo_location = obj.geo_obj.location
# Get mirrors
mirrors_list = list(obj.geo_obj.mirrors.values_list('name', flat=True))
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,
"mirrors": ", ".join(mirrors_list) if mirrors_list else "-",
"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()
# Save ManyToMany relationship for mirrors
if geo_form.is_valid():
geo_instance.mirrors.set(geo_form.cleaned_data["mirrors"])
def get_or_create_geo_instance(self):
"""Gets or creates Geo instance."""
if hasattr(self.object, "geo_obj") and self.object.geo_obj:
return self.object.geo_obj
return Geo(objitem=self.object)
class ObjItemUpdateView(ObjItemFormView):
"""View for editing ObjItem."""
success_message = "Объект успешно сохранён!"
def set_user_fields(self):
self.object.updated_by = self.request.user.customuser
class ObjItemCreateView(ObjItemFormView, CreateView):
"""View for creating ObjItem."""
success_message = "Объект успешно создан!"
def set_user_fields(self):
self.object.created_by = self.request.user.customuser
self.object.updated_by = self.request.user.customuser
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
"""View for deleting ObjItem."""
model = ObjItem
template_name = "mainapp/objitem_confirm_delete.html"
success_url = reverse_lazy("mainapp:objitem_list")
success_message = "Объект успешно удалён!"
required_roles = ["admin", "moderator"]
def get_success_url(self):
"""Returns URL with saved filter parameters."""
if self.request.GET:
from urllib.parse import urlencode
query_string = urlencode(self.request.GET)
return reverse_lazy("mainapp:objitem_list") + '?' + query_string
return reverse_lazy("mainapp:objitem_list")
class ObjItemDetailView(LoginRequiredMixin, View):
"""
View for displaying ObjItem details in read-only mode.
Available to all authenticated users, displays data in read-only mode.
"""
def get(self, request, pk):
obj = ObjItem.objects.filter(pk=pk).select_related(
'geo_obj',
'updated_by__user',
'created_by__user',
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
'parameter_obj__modulation',
'parameter_obj__standard',
).first()
if not obj:
from django.http import Http404
raise Http404("Объект не найден")
# Save return parameters for "Back" button
return_params = request.GET.get('return_params', '')
context = {
'object': obj,
'return_params': return_params
}
return render(request, "mainapp/objitem_detail.html", context)

View File

@@ -0,0 +1,190 @@
"""
Source related views.
"""
from datetime import datetime
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Count
from django.shortcuts import render
from django.views import View
from ..models import Source
from ..utils import parse_pagination_params
class SourceListView(LoginRequiredMixin, View):
"""
View for displaying a list of sources (Source).
"""
def get(self, request):
# Get pagination parameters
page_number, items_per_page = parse_pagination_params(request)
# Get sorting parameters
sort_param = request.GET.get("sort", "-created_at")
# Get filter parameters
search_query = request.GET.get("search", "").strip()
has_coords_average = request.GET.get("has_coords_average")
has_coords_kupsat = request.GET.get("has_coords_kupsat")
has_coords_valid = request.GET.get("has_coords_valid")
has_coords_reference = request.GET.get("has_coords_reference")
objitem_count_min = request.GET.get("objitem_count_min", "").strip()
objitem_count_max = request.GET.get("objitem_count_max", "").strip()
date_from = request.GET.get("date_from", "").strip()
date_to = request.GET.get("date_to", "").strip()
# Get all Source objects with query optimization
sources = Source.objects.select_related(
'created_by__user',
'updated_by__user'
).prefetch_related(
'source_objitems',
'source_objitems__parameter_obj',
'source_objitems__geo_obj'
).annotate(
objitem_count=Count('source_objitems')
)
# Apply filters
# Filter by coords_average presence
if has_coords_average == "1":
sources = sources.filter(coords_average__isnull=False)
elif has_coords_average == "0":
sources = sources.filter(coords_average__isnull=True)
# Filter by coords_kupsat presence
if has_coords_kupsat == "1":
sources = sources.filter(coords_kupsat__isnull=False)
elif has_coords_kupsat == "0":
sources = sources.filter(coords_kupsat__isnull=True)
# Filter by coords_valid presence
if has_coords_valid == "1":
sources = sources.filter(coords_valid__isnull=False)
elif has_coords_valid == "0":
sources = sources.filter(coords_valid__isnull=True)
# Filter by coords_reference presence
if has_coords_reference == "1":
sources = sources.filter(coords_reference__isnull=False)
elif has_coords_reference == "0":
sources = sources.filter(coords_reference__isnull=True)
# Filter by ObjItem count
if objitem_count_min:
try:
min_count = int(objitem_count_min)
sources = sources.filter(objitem_count__gte=min_count)
except ValueError:
pass
if objitem_count_max:
try:
max_count = int(objitem_count_max)
sources = sources.filter(objitem_count__lte=max_count)
except ValueError:
pass
# Filter by creation date range
if date_from:
try:
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
sources = sources.filter(created_at__gte=date_from_obj)
except (ValueError, TypeError):
pass
if date_to:
try:
from datetime import timedelta
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d")
# Add one day to include entire end date
date_to_obj = date_to_obj + timedelta(days=1)
sources = sources.filter(created_at__lt=date_to_obj)
except (ValueError, TypeError):
pass
# Search by ID
if search_query:
try:
search_id = int(search_query)
sources = sources.filter(id=search_id)
except ValueError:
# If not a number, ignore
pass
# Apply sorting
valid_sort_fields = {
"id": "id",
"-id": "-id",
"created_at": "created_at",
"-created_at": "-created_at",
"updated_at": "updated_at",
"-updated_at": "-updated_at",
"objitem_count": "objitem_count",
"-objitem_count": "-objitem_count",
}
if sort_param in valid_sort_fields:
sources = sources.order_by(valid_sort_fields[sort_param])
# Create paginator
paginator = Paginator(sources, items_per_page)
page_obj = paginator.get_page(page_number)
# Prepare data for display
processed_sources = []
for source in page_obj:
# Format coordinates
def format_coords(point):
if point:
longitude = point.coords[0]
latitude = point.coords[1]
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}"
return "-"
coords_average_str = format_coords(source.coords_average)
coords_kupsat_str = format_coords(source.coords_kupsat)
coords_valid_str = format_coords(source.coords_valid)
coords_reference_str = format_coords(source.coords_reference)
# Get count of related ObjItems
objitem_count = source.objitem_count
processed_sources.append({
'id': source.id,
'coords_average': coords_average_str,
'coords_kupsat': coords_kupsat_str,
'coords_valid': coords_valid_str,
'coords_reference': coords_reference_str,
'objitem_count': objitem_count,
'created_at': source.created_at,
'updated_at': source.updated_at,
'created_by': source.created_by,
'updated_by': source.updated_by,
})
# Prepare context for template
context = {
'page_obj': page_obj,
'processed_sources': processed_sources,
'items_per_page': items_per_page,
'available_items_per_page': [50, 100, 500, 1000],
'sort': sort_param,
'search_query': search_query,
'has_coords_average': has_coords_average,
'has_coords_kupsat': has_coords_kupsat,
'has_coords_valid': has_coords_valid,
'has_coords_reference': has_coords_reference,
'objitem_count_min': objitem_count_min,
'objitem_count_max': objitem_count_max,
'date_from': date_from,
'date_to': date_to,
'full_width_page': True,
}
return render(request, "mainapp/source_list.html", context)

View File

@@ -61,15 +61,6 @@ class AddSatellitesView(LoginRequiredMixin, View):
return redirect("mainapp:home")
# 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
@@ -915,6 +906,7 @@ class ObjItemListView(LoginRequiredMixin, View):
objects = (
ObjItem.objects.select_related(
"geo_obj",
"source",
"updated_by__user",
"created_by__user",
"lyngsat_source",
@@ -933,6 +925,7 @@ class ObjItemListView(LoginRequiredMixin, View):
else:
objects = ObjItem.objects.select_related(
"geo_obj",
"source",
"updated_by__user",
"created_by__user",
"lyngsat_source",
@@ -1021,14 +1014,14 @@ class ObjItemListView(LoginRequiredMixin, View):
)
if has_kupsat == "1":
objects = objects.filter(geo_obj__coords_kupsat__isnull=False)
objects = objects.filter(source__coords_kupsat__isnull=False)
elif has_kupsat == "0":
objects = objects.filter(geo_obj__coords_kupsat__isnull=True)
objects = objects.filter(source__coords_kupsat__isnull=True)
if has_valid == "1":
objects = objects.filter(geo_obj__coords_valid__isnull=False)
objects = objects.filter(source__coords_valid__isnull=False)
elif has_valid == "0":
objects = objects.filter(geo_obj__coords_valid__isnull=True)
objects = objects.filter(source__coords_valid__isnull=True)
# Date filter for geo_obj timestamp
date_from = request.GET.get("date_from")
@@ -1145,33 +1138,6 @@ class ObjItemListView(LoginRequiredMixin, View):
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
geo_coords = f"{lat} {lon}"
if obj.geo_obj.coords_kupsat:
longitude = obj.geo_obj.coords_kupsat.coords[0]
latitude = obj.geo_obj.coords_kupsat.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"
kupsat_coords = f"{lat} {lon}"
elif obj.geo_obj.coords_kupsat is not None:
kupsat_coords = "-"
if obj.geo_obj.coords_valid:
longitude = obj.geo_obj.coords_valid.coords[0]
latitude = obj.geo_obj.coords_valid.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"
valid_coords = f"{lat} {lon}"
elif obj.geo_obj.coords_valid is not None:
valid_coords = "-"
if obj.geo_obj.distance_coords_kup is not None:
distance_geo_kup = f"{obj.geo_obj.distance_coords_kup:.3f}"
if obj.geo_obj.distance_coords_valid is not None:
distance_geo_valid = f"{obj.geo_obj.distance_coords_valid:.3f}"
if obj.geo_obj.distance_kup_valid is not None:
distance_kup_valid = f"{obj.geo_obj.distance_kup_valid:.3f}"
satellite_name = "-"
frequency = "-"
freq_range = "-"
@@ -1439,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)