Реструктуризация views
This commit is contained in:
@@ -16,7 +16,7 @@ Including another URLconf
|
|||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from mainapp import views
|
from mainapp.views import custom_logout
|
||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||||
|
|
||||||
@@ -26,5 +26,5 @@ urlpatterns = [
|
|||||||
path('', include('mapsapp.urls', namespace='mapsapp')),
|
path('', include('mapsapp.urls', namespace='mapsapp')),
|
||||||
# Authentication URLs
|
# Authentication URLs
|
||||||
path('login/', auth_views.LoginView.as_view(), name='login'),
|
path('login/', auth_views.LoginView.as_view(), name='login'),
|
||||||
path('logout/', views.custom_logout, name='logout'),
|
path('logout/', custom_logout, name='logout'),
|
||||||
] + debug_toolbar_urls()
|
] + debug_toolbar_urls()
|
||||||
|
|||||||
@@ -59,20 +59,16 @@ class CoordinateProcessingMixin:
|
|||||||
Предоставляет методы для извлечения и обработки координат различных типов
|
Предоставляет методы для извлечения и обработки координат различных типов
|
||||||
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
|
(геолокация, кубсат, оперативники) из POST запроса и применения их к объекту Geo.
|
||||||
|
|
||||||
Example:
|
Note: Координаты Кубсата и оперативников теперь хранятся в модели Source,
|
||||||
class MyFormView(CoordinateProcessingMixin, FormView):
|
а не в модели Geo, но для совместимости в форме все еще могут быть поля
|
||||||
def form_valid(self, form):
|
для этих координат.
|
||||||
geo_instance = Geo()
|
|
||||||
self.process_coordinates(geo_instance)
|
|
||||||
geo_instance.save()
|
|
||||||
return super().form_valid(form)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
|
def process_coordinates(self, geo_instance, prefix: str = "geo") -> None:
|
||||||
"""
|
"""
|
||||||
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
|
Обрабатывает координаты из POST данных и применяет их к объекту Geo.
|
||||||
|
|
||||||
Извлекает координаты геолокации, кубсата и оперативников из POST запроса
|
Извлекает координаты геолокации из POST запроса
|
||||||
и устанавливает соответствующие поля объекта Geo.
|
и устанавливает соответствующие поля объекта Geo.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -82,28 +78,12 @@ class CoordinateProcessingMixin:
|
|||||||
Note:
|
Note:
|
||||||
Метод ожидает следующие поля в request.POST:
|
Метод ожидает следующие поля в request.POST:
|
||||||
- geo_longitude, geo_latitude: координаты геолокации
|
- geo_longitude, geo_latitude: координаты геолокации
|
||||||
- kupsat_longitude, kupsat_latitude: координаты кубсата
|
|
||||||
- valid_longitude, valid_latitude: координаты оперативников
|
|
||||||
"""
|
"""
|
||||||
# Обрабатываем координаты геолокации
|
# Обрабатываем координаты геолокации
|
||||||
geo_coords = self._extract_coordinates("geo")
|
geo_coords = self._extract_coordinates("geo")
|
||||||
if geo_coords:
|
if geo_coords:
|
||||||
geo_instance.coords = Point(geo_coords[0], geo_coords[1], srid=4326)
|
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]]:
|
def _extract_coordinates(self, coord_type: str) -> Optional[Tuple[float, float]]:
|
||||||
"""
|
"""
|
||||||
Извлекает координаты указанного типа из POST данных.
|
Извлекает координаты указанного типа из POST данных.
|
||||||
|
|||||||
@@ -220,52 +220,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Координаты Кубсата -->
|
|
||||||
<div class="coord-group">
|
|
||||||
<div class="coord-group-header">Координаты Кубсата</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Долгота:</label>
|
|
||||||
<div class="readonly-field">
|
|
||||||
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x|floatformat:6 }}{% else %}-{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Широта:</label>
|
|
||||||
<div class="readonly-field">
|
|
||||||
{% if object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y|floatformat:6 }}{% else %}-{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Координаты оперативников -->
|
|
||||||
<div class="coord-group">
|
|
||||||
<div class="coord-group-header">Координаты оперативников</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Долгота:</label>
|
|
||||||
<div class="readonly-field">
|
|
||||||
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x|floatformat:6 }}{% else %}-{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Широта:</label>
|
|
||||||
<div class="readonly-field">
|
|
||||||
{% if object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y|floatformat:6 }}{% else %}-{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -298,45 +252,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Расстояние гео-кубсат, км:</label>
|
|
||||||
<div class="readonly-field">
|
|
||||||
{% if object.geo_obj.distance_coords_kup is not None %}
|
|
||||||
{{ object.geo_obj.distance_coords_kup|floatformat:2 }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Расстояние гео-опер, км:</label>
|
|
||||||
<div class="readonly-field">
|
|
||||||
{% if object.geo_obj.distance_coords_valid is not None %}
|
|
||||||
{{ object.geo_obj.distance_coords_valid|floatformat:2 }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Расстояние кубсат-опер, км:</label>
|
|
||||||
<div class="readonly-field">
|
|
||||||
{% if object.geo_obj.distance_kup_valid is not None %}
|
|
||||||
{{ object.geo_obj.distance_kup_valid|floatformat:2 }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Нет данных о геолокации</p>
|
<p>Нет данных о геолокации</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -368,17 +283,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
// Определяем цвета для маркеров
|
|
||||||
const colors = {
|
|
||||||
geo: 'blue',
|
|
||||||
kupsat: 'red',
|
|
||||||
valid: 'green'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для создания иконки маркера
|
// Функция для создания иконки маркера
|
||||||
function createMarkerIcon(color) {
|
function createMarkerIcon() {
|
||||||
return L.icon({
|
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' %}`,
|
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
||||||
iconSize: [25, 41],
|
iconSize: [25, 41],
|
||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
@@ -387,18 +295,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 %}
|
{% if object.geo_obj and object.geo_obj.coords %}
|
||||||
const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
|
const geoLat = {{ object.geo_obj.coords.y|unlocalize }};
|
||||||
@@ -408,66 +304,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const geoLng = 37.62;
|
const geoLng = 37.62;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if object.geo_obj and object.geo_obj.coords_kupsat %}
|
// Создаем маркер геолокации
|
||||||
const kupsatLat = {{ object.geo_obj.coords_kupsat.y|unlocalize }};
|
const marker = L.marker([geoLat, geoLng], {
|
||||||
const kupsatLng = {{ object.geo_obj.coords_kupsat.x|unlocalize }};
|
draggable: false,
|
||||||
{% else %}
|
icon: createMarkerIcon(),
|
||||||
const kupsatLat = 55.75;
|
title: 'Геолокация'
|
||||||
const kupsatLng = 37.61;
|
}).addTo(map);
|
||||||
{% endif %}
|
marker.bindPopup('Геолокация');
|
||||||
|
|
||||||
{% if object.geo_obj and object.geo_obj.coords_valid %}
|
// Центрируем карту на маркере
|
||||||
const validLat = {{ object.geo_obj.coords_valid.y|unlocalize }};
|
map.setView(marker.getLatLng(), 10);
|
||||||
const validLng = {{ object.geo_obj.coords_valid.x|unlocalize }};
|
|
||||||
{% else %}
|
|
||||||
const validLat = 55.75;
|
|
||||||
const validLng = 37.63;
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
// Создаем маркеры
|
|
||||||
markers.geo = createMarker(
|
|
||||||
[geoLat, geoLng],
|
|
||||||
colors.geo,
|
|
||||||
'Геолокация'
|
|
||||||
);
|
|
||||||
|
|
||||||
markers.kupsat = createMarker(
|
|
||||||
[kupsatLat, kupsatLng],
|
|
||||||
colors.kupsat,
|
|
||||||
'Кубсат'
|
|
||||||
);
|
|
||||||
|
|
||||||
markers.valid = createMarker(
|
|
||||||
[validLat, validLng],
|
|
||||||
colors.valid,
|
|
||||||
'Оперативник'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Центрируем карту на первом маркере
|
|
||||||
if (map.hasLayer(markers.geo)) {
|
|
||||||
map.setView(markers.geo.getLatLng(), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Легенда
|
|
||||||
const legend = L.control({ position: 'bottomright' });
|
|
||||||
|
|
||||||
legend.onAdd = function() {
|
|
||||||
const div = L.DomUtil.create('div', 'info legend');
|
|
||||||
div.style.fontSize = '14px';
|
|
||||||
div.style.backgroundColor = 'white';
|
|
||||||
div.style.padding = '10px';
|
|
||||||
div.style.borderRadius = '4px';
|
|
||||||
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
|
||||||
div.innerHTML = `
|
|
||||||
<h5>Легенда</h5>
|
|
||||||
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
|
|
||||||
<div><span style="color: red; font-weight: bold;">•</span> Кубсат</div>
|
|
||||||
<div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
|
|
||||||
`;
|
|
||||||
return div;
|
|
||||||
};
|
|
||||||
|
|
||||||
legend.addTo(map);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -202,52 +202,6 @@
|
|||||||
</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_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="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% include 'mainapp/components/_form_field.html' with field=geo_form.location %}
|
{% include 'mainapp/components/_form_field.html' with field=geo_form.location %}
|
||||||
@@ -281,47 +235,6 @@
|
|||||||
{% include 'mainapp/components/_form_field.html' with field=geo_form.is_average %}
|
{% include 'mainapp/components/_form_field.html' with field=geo_form.is_average %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -344,17 +257,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
// Определяем цвета для маркеров
|
|
||||||
const colors = {
|
|
||||||
geo: 'blue',
|
|
||||||
kupsat: 'red',
|
|
||||||
valid: 'green'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для создания иконки маркера
|
// Функция для создания иконки маркера
|
||||||
function createMarkerIcon(color) {
|
function createMarkerIcon() {
|
||||||
return L.icon({
|
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' %}`,
|
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
||||||
iconSize: [25, 41],
|
iconSize: [25, 41],
|
||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
@@ -365,20 +271,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const editableLayerGroup = new L.FeatureGroup();
|
const editableLayerGroup = new L.FeatureGroup();
|
||||||
map.addLayer(editableLayerGroup);
|
map.addLayer(editableLayerGroup);
|
||||||
|
|
||||||
// Маркеры
|
// Маркер геолокации
|
||||||
const markers = {};
|
const marker = L.marker([55.75, 37.62], {
|
||||||
function createMarker(latFieldId, lngFieldId, position, color, name) {
|
|
||||||
const marker = L.marker(position, {
|
|
||||||
draggable: false,
|
draggable: false,
|
||||||
icon: createMarkerIcon(color),
|
icon: createMarkerIcon(),
|
||||||
title: name
|
title: 'Геолокация'
|
||||||
}).addTo(editableLayerGroup);
|
}).addTo(editableLayerGroup);
|
||||||
marker.bindPopup(name);
|
marker.bindPopup('Геолокация');
|
||||||
|
|
||||||
// Синхронизация при изменении формы
|
// Синхронизация при изменении формы
|
||||||
function syncFromForm() {
|
function syncFromForm() {
|
||||||
const lat = parseFloat(document.getElementById(latFieldId).value);
|
const lat = parseFloat(document.getElementById('id_geo_latitude').value);
|
||||||
const lng = parseFloat(document.getElementById(lngFieldId).value);
|
const lng = parseFloat(document.getElementById('id_geo_longitude').value);
|
||||||
if (!isNaN(lat) && !isNaN(lng)) {
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
marker.setLatLng([lat, lng]);
|
marker.setLatLng([lat, lng]);
|
||||||
}
|
}
|
||||||
@@ -387,8 +291,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Синхронизация при перетаскивании (только если активировано)
|
// Синхронизация при перетаскивании (только если активировано)
|
||||||
marker.on('dragend', function(event) {
|
marker.on('dragend', function(event) {
|
||||||
const latLng = event.target.getLatLng();
|
const latLng = event.target.getLatLng();
|
||||||
document.getElementById(latFieldId).value = latLng.lat.toFixed(6);
|
document.getElementById('id_geo_latitude').value = latLng.lat.toFixed(6);
|
||||||
document.getElementById(lngFieldId).value = latLng.lng.toFixed(6);
|
document.getElementById('id_geo_longitude').value = latLng.lng.toFixed(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем методы для управления
|
// Добавляем методы для управления
|
||||||
@@ -404,53 +308,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
marker.syncFromForm = syncFromForm;
|
marker.syncFromForm = syncFromForm;
|
||||||
|
|
||||||
return marker;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем маркеры
|
|
||||||
markers.geo = createMarker(
|
|
||||||
'id_geo_latitude',
|
|
||||||
'id_geo_longitude',
|
|
||||||
[55.75, 37.62],
|
|
||||||
colors.geo,
|
|
||||||
'Геолокация'
|
|
||||||
);
|
|
||||||
|
|
||||||
markers.kupsat = createMarker(
|
|
||||||
'id_kupsat_latitude',
|
|
||||||
'id_kupsat_longitude',
|
|
||||||
[55.75, 37.61],
|
|
||||||
colors.kupsat,
|
|
||||||
'Кубсат'
|
|
||||||
);
|
|
||||||
|
|
||||||
markers.valid = createMarker(
|
|
||||||
'id_valid_latitude',
|
|
||||||
'id_valid_longitude',
|
|
||||||
[55.75, 37.63],
|
|
||||||
colors.valid,
|
|
||||||
'Оперативник'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Устанавливаем начальные координаты из полей формы
|
// Устанавливаем начальные координаты из полей формы
|
||||||
function initMarkersFromForm() {
|
function initMarkersFromForm() {
|
||||||
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
|
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
|
||||||
const geoLng = parseFloat(document.getElementById('id_geo_longitude').value) || 37.62;
|
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;
|
map.setView(marker.getLatLng(), 10);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Настройка формы для синхронизации с маркерами
|
// Настройка формы для синхронизации с маркером
|
||||||
function setupFormChange(latFieldId, lngFieldId, marker) {
|
function setupFormChange(latFieldId, lngFieldId, marker) {
|
||||||
const latField = document.getElementById(latFieldId);
|
const latField = document.getElementById(latFieldId);
|
||||||
const lngField = document.getElementById(lngFieldId);
|
const lngField = document.getElementById(lngFieldId);
|
||||||
@@ -470,10 +338,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Инициализация
|
// Инициализация
|
||||||
initMarkersFromForm();
|
initMarkersFromForm();
|
||||||
// Настройка формы для синхронизации с маркерами
|
// Настройка формы для синхронизации с маркером
|
||||||
setupFormChange('id_geo_latitude', 'id_geo_longitude', markers.geo);
|
setupFormChange('id_geo_latitude', 'id_geo_longitude', marker);
|
||||||
setupFormChange('id_kupsat_latitude', 'id_kupsat_longitude', markers.kupsat);
|
|
||||||
setupFormChange('id_valid_latitude', 'id_valid_longitude', markers.valid);
|
|
||||||
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
|
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
|
||||||
// Кнопки редактирования
|
// Кнопки редактирования
|
||||||
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
|
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
|
||||||
@@ -497,11 +363,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let isEditing = false;
|
let isEditing = false;
|
||||||
|
|
||||||
// Сохраняем начальные координаты для отмены
|
// Сохраняем начальные координаты для отмены
|
||||||
const initialPositions = {
|
const initialPosition = marker.getLatLng();
|
||||||
geo: markers.geo.getLatLng(),
|
|
||||||
kupsat: markers.kupsat.getLatLng(),
|
|
||||||
valid: markers.valid.getLatLng()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Включение редактирования
|
// Включение редактирования
|
||||||
document.getElementById('edit-btn').addEventListener('click', function() {
|
document.getElementById('edit-btn').addEventListener('click', function() {
|
||||||
@@ -512,15 +374,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.getElementById('save-btn').disabled = false;
|
document.getElementById('save-btn').disabled = false;
|
||||||
document.getElementById('cancel-btn').disabled = false;
|
document.getElementById('cancel-btn').disabled = false;
|
||||||
|
|
||||||
// Включаем drag для всех маркеров
|
// Включаем drag для маркера
|
||||||
Object.values(markers).forEach(marker => {
|
|
||||||
marker.enableEditing();
|
marker.enableEditing();
|
||||||
});
|
|
||||||
|
|
||||||
// Показываем подсказку
|
// Показываем подсказку
|
||||||
L.popup()
|
L.popup()
|
||||||
.setLatLng(map.getCenter())
|
.setLatLng(map.getCenter())
|
||||||
.setContent('Перетаскивайте маркеры. Нажмите "Сохранить" или "Отмена".')
|
.setContent('Перетаскивайте маркер. Нажмите "Сохранить" или "Отмена".')
|
||||||
.openOn(map);
|
.openOn(map);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -534,14 +394,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.getElementById('cancel-btn').disabled = true;
|
document.getElementById('cancel-btn').disabled = true;
|
||||||
|
|
||||||
// Отключаем редактирование
|
// Отключаем редактирование
|
||||||
Object.values(markers).forEach(marker => {
|
|
||||||
marker.disableEditing();
|
marker.disableEditing();
|
||||||
});
|
|
||||||
|
|
||||||
// Обновляем начальные позиции
|
// Обновляем начальную позицию
|
||||||
initialPositions.geo = markers.geo.getLatLng();
|
initialPosition.lat = marker.getLatLng().lat;
|
||||||
initialPositions.kupsat = markers.kupsat.getLatLng();
|
initialPosition.lng = marker.getLatLng().lng;
|
||||||
initialPositions.valid = markers.valid.getLatLng();
|
|
||||||
|
|
||||||
// Убираем попап подсказки
|
// Убираем попап подсказки
|
||||||
map.closePopup();
|
map.closePopup();
|
||||||
@@ -556,25 +413,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.getElementById('save-btn').disabled = true;
|
document.getElementById('save-btn').disabled = true;
|
||||||
document.getElementById('cancel-btn').disabled = true;
|
document.getElementById('cancel-btn').disabled = true;
|
||||||
|
|
||||||
// Возвращаем маркеры на исходные позиции
|
// Возвращаем маркер на исходную позицию
|
||||||
markers.geo.setLatLng(initialPositions.geo);
|
marker.setLatLng(initialPosition);
|
||||||
markers.kupsat.setLatLng(initialPositions.kupsat);
|
|
||||||
markers.valid.setLatLng(initialPositions.valid);
|
|
||||||
|
|
||||||
// Отключаем редактирование
|
// Отключаем редактирование
|
||||||
Object.values(markers).forEach(marker => {
|
|
||||||
marker.disableEditing();
|
marker.disableEditing();
|
||||||
});
|
|
||||||
|
|
||||||
// Синхронизируем форму с исходными значениями
|
// Синхронизируем форму с исходным значением
|
||||||
document.getElementById('id_geo_latitude').value = initialPositions.geo.lat.toFixed(6);
|
document.getElementById('id_geo_latitude').value = initialPosition.lat.toFixed(6);
|
||||||
document.getElementById('id_geo_longitude').value = initialPositions.geo.lng.toFixed(6);
|
document.getElementById('id_geo_longitude').value = initialPosition.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);
|
|
||||||
map.closePopup();
|
map.closePopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -591,8 +438,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<h5>Легенда</h5>
|
<h5>Легенда</h5>
|
||||||
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
|
<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;
|
return div;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,37 +1,67 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
from .views import (
|
||||||
|
ActionsPageView,
|
||||||
|
AddSatellitesView,
|
||||||
|
AddTranspondersView,
|
||||||
|
ClusterTestView,
|
||||||
|
ClearLyngsatCacheView,
|
||||||
|
DeleteSelectedObjectsView,
|
||||||
|
FillLyngsatDataView,
|
||||||
|
GetLocationsView,
|
||||||
|
LinkLyngsatSourcesView,
|
||||||
|
LinkVchSigmaView,
|
||||||
|
LoadCsvDataView,
|
||||||
|
LoadExcelDataView,
|
||||||
|
LyngsatDataAPIView,
|
||||||
|
LyngsatTaskStatusAPIView,
|
||||||
|
LyngsatTaskStatusView,
|
||||||
|
ObjItemCreateView,
|
||||||
|
ObjItemDeleteView,
|
||||||
|
ObjItemDetailView,
|
||||||
|
ObjItemListView,
|
||||||
|
ObjItemUpdateView,
|
||||||
|
ProcessKubsatView,
|
||||||
|
ShowMapView,
|
||||||
|
ShowSelectedObjectsMapView,
|
||||||
|
SourceListView,
|
||||||
|
SourceObjItemsAPIView,
|
||||||
|
SigmaParameterDataAPIView,
|
||||||
|
UploadVchLoadView,
|
||||||
|
custom_logout,
|
||||||
|
)
|
||||||
|
|
||||||
app_name = 'mainapp'
|
app_name = 'mainapp'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.SourceListView.as_view(), name='home'),
|
path('', SourceListView.as_view(), name='home'),
|
||||||
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'),
|
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
||||||
path('actions/', views.ActionsPageView.as_view(), name='actions'),
|
path('actions/', ActionsPageView.as_view(), name='actions'),
|
||||||
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
|
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||||
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'),
|
path('satellites', AddSatellitesView.as_view(), name='add_sats'),
|
||||||
path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
|
path('api/locations/<int:sat_id>/geojson/', GetLocationsView.as_view(), name='locations_by_id'),
|
||||||
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
|
path('transponders', AddTranspondersView.as_view(), name='add_trans'),
|
||||||
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
|
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||||
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
|
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
|
||||||
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||||
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||||
path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
|
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
||||||
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
|
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
||||||
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
|
path('vch-link/', LinkVchSigmaView.as_view(), name='link_vch_sigma'),
|
||||||
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
|
path('link-lyngsat/', LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
|
||||||
path('api/lyngsat/<int:lyngsat_id>/', views.LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
||||||
path('api/sigma-parameter/<int:parameter_id>/', views.SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
||||||
path('api/source/<int:source_id>/objitems/', views.SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||||
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
|
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||||
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
||||||
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||||
path('object/<int:pk>/', views.ObjItemDetailView.as_view(), name='objitem_detail'),
|
path('object/<int:pk>/', ObjItemDetailView.as_view(), name='objitem_detail'),
|
||||||
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
|
path('object/<int:pk>/delete/', ObjItemDeleteView.as_view(), name='objitem_delete'),
|
||||||
path('fill-lyngsat-data/', views.FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
|
path('fill-lyngsat-data/', FillLyngsatDataView.as_view(), name='fill_lyngsat_data'),
|
||||||
path('lyngsat-task-status/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
path('lyngsat-task-status/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||||
path('lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
path('lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusView.as_view(), name='lyngsat_task_status'),
|
||||||
path('api/lyngsat-task-status/<str:task_id>/', views.LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
|
path('api/lyngsat-task-status/<str:task_id>/', LyngsatTaskStatusAPIView.as_view(), name='lyngsat_task_status_api'),
|
||||||
path('clear-lyngsat-cache/', views.ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
|
path('clear-lyngsat-cache/', ClearLyngsatCacheView.as_view(), name='clear_lyngsat_cache'),
|
||||||
|
path('logout/', custom_logout, name='logout'),
|
||||||
]
|
]
|
||||||
75
dbapp/mainapp/views/README.md
Normal file
75
dbapp/mainapp/views/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Views Module Structure
|
||||||
|
|
||||||
|
This directory contains the refactored views from the original monolithic `views.py` file.
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
### `__init__.py`
|
||||||
|
Central import file that exports all views for easy access. This allows other modules to import views using:
|
||||||
|
```python
|
||||||
|
from mainapp.views import ObjItemListView, custom_logout
|
||||||
|
```
|
||||||
|
|
||||||
|
### `base.py`
|
||||||
|
Basic views and utilities:
|
||||||
|
- `ActionsPageView` - Displays the actions page
|
||||||
|
- `custom_logout()` - Custom logout function
|
||||||
|
|
||||||
|
### `objitem.py`
|
||||||
|
ObjItem CRUD operations and related views:
|
||||||
|
- `ObjItemListView` - List view with filtering and pagination
|
||||||
|
- `ObjItemFormView` - Base class for create/update operations
|
||||||
|
- `ObjItemCreateView` - Create new ObjItem
|
||||||
|
- `ObjItemUpdateView` - Update existing ObjItem
|
||||||
|
- `ObjItemDeleteView` - Delete ObjItem
|
||||||
|
- `ObjItemDetailView` - Read-only detail view
|
||||||
|
- `DeleteSelectedObjectsView` - Bulk delete operation
|
||||||
|
|
||||||
|
### `data_import.py`
|
||||||
|
Data import views for various formats:
|
||||||
|
- `AddSatellitesView` - Add satellites to database
|
||||||
|
- `AddTranspondersView` - Upload and parse transponder data from XML
|
||||||
|
- `LoadExcelDataView` - Load data from Excel files
|
||||||
|
- `LoadCsvDataView` - Load data from CSV files
|
||||||
|
- `UploadVchLoadView` - Upload VCH load data from HTML
|
||||||
|
- `LinkVchSigmaView` - Link VCH data with Sigma parameters
|
||||||
|
- `ProcessKubsatView` - Process Kubsat event data
|
||||||
|
|
||||||
|
### `api.py`
|
||||||
|
API endpoints for AJAX requests:
|
||||||
|
- `GetLocationsView` - Get locations by satellite ID in GeoJSON format
|
||||||
|
- `LyngsatDataAPIView` - Get LyngSat source data
|
||||||
|
- `SigmaParameterDataAPIView` - Get SigmaParameter data
|
||||||
|
- `SourceObjItemsAPIView` - Get ObjItems related to a Source
|
||||||
|
- `LyngsatTaskStatusAPIView` - Get Celery task status
|
||||||
|
|
||||||
|
### `lyngsat.py`
|
||||||
|
LyngSat related views:
|
||||||
|
- `LinkLyngsatSourcesView` - Link LyngSat sources to objects
|
||||||
|
- `FillLyngsatDataView` - Fill data from Lyngsat website
|
||||||
|
- `LyngsatTaskStatusView` - Track Lyngsat data filling task status
|
||||||
|
- `ClearLyngsatCacheView` - Clear LyngSat cache
|
||||||
|
|
||||||
|
### `source.py`
|
||||||
|
Source related views:
|
||||||
|
- `SourceListView` - List view for Source objects with filtering
|
||||||
|
|
||||||
|
### `map.py`
|
||||||
|
Map related views:
|
||||||
|
- `ShowMapView` - Display objects on map (admin interface)
|
||||||
|
- `ShowSelectedObjectsMapView` - Display selected objects on map
|
||||||
|
- `ClusterTestView` - Test view for clustering functionality
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
The original `views.py` has been renamed to `views_old.py` as a backup. All imports have been updated in:
|
||||||
|
- `dbapp/mainapp/urls.py`
|
||||||
|
- `dbapp/dbapp/urls.py`
|
||||||
|
|
||||||
|
## Benefits of This Structure
|
||||||
|
|
||||||
|
1. **Better Organization** - Related views are grouped together
|
||||||
|
2. **Easier Maintenance** - Smaller files are easier to navigate and modify
|
||||||
|
3. **Clear Responsibilities** - Each file has a specific purpose
|
||||||
|
4. **Improved Testability** - Easier to write focused unit tests
|
||||||
|
5. **Better Collaboration** - Multiple developers can work on different files without conflicts
|
||||||
72
dbapp/mainapp/views/__init__.py
Normal file
72
dbapp/mainapp/views/__init__.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Import all views for easy access
|
||||||
|
from .base import ActionsPageView, custom_logout
|
||||||
|
from .objitem import (
|
||||||
|
ObjItemListView,
|
||||||
|
ObjItemCreateView,
|
||||||
|
ObjItemUpdateView,
|
||||||
|
ObjItemDeleteView,
|
||||||
|
ObjItemDetailView,
|
||||||
|
DeleteSelectedObjectsView,
|
||||||
|
)
|
||||||
|
from .data_import import (
|
||||||
|
AddSatellitesView,
|
||||||
|
AddTranspondersView,
|
||||||
|
LoadExcelDataView,
|
||||||
|
LoadCsvDataView,
|
||||||
|
UploadVchLoadView,
|
||||||
|
LinkVchSigmaView,
|
||||||
|
ProcessKubsatView,
|
||||||
|
)
|
||||||
|
from .api import (
|
||||||
|
GetLocationsView,
|
||||||
|
LyngsatDataAPIView,
|
||||||
|
SigmaParameterDataAPIView,
|
||||||
|
SourceObjItemsAPIView,
|
||||||
|
LyngsatTaskStatusAPIView,
|
||||||
|
)
|
||||||
|
from .lyngsat import (
|
||||||
|
LinkLyngsatSourcesView,
|
||||||
|
FillLyngsatDataView,
|
||||||
|
LyngsatTaskStatusView,
|
||||||
|
ClearLyngsatCacheView,
|
||||||
|
)
|
||||||
|
from .source import SourceListView
|
||||||
|
from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Base
|
||||||
|
'ActionsPageView',
|
||||||
|
'custom_logout',
|
||||||
|
# ObjItem
|
||||||
|
'ObjItemListView',
|
||||||
|
'ObjItemCreateView',
|
||||||
|
'ObjItemUpdateView',
|
||||||
|
'ObjItemDeleteView',
|
||||||
|
'ObjItemDetailView',
|
||||||
|
'DeleteSelectedObjectsView',
|
||||||
|
# Data Import
|
||||||
|
'AddSatellitesView',
|
||||||
|
'AddTranspondersView',
|
||||||
|
'LoadExcelDataView',
|
||||||
|
'LoadCsvDataView',
|
||||||
|
'UploadVchLoadView',
|
||||||
|
'LinkVchSigmaView',
|
||||||
|
'ProcessKubsatView',
|
||||||
|
# API
|
||||||
|
'GetLocationsView',
|
||||||
|
'LyngsatDataAPIView',
|
||||||
|
'SigmaParameterDataAPIView',
|
||||||
|
'SourceObjItemsAPIView',
|
||||||
|
'LyngsatTaskStatusAPIView',
|
||||||
|
# LyngSat
|
||||||
|
'LinkLyngsatSourcesView',
|
||||||
|
'FillLyngsatDataView',
|
||||||
|
'LyngsatTaskStatusView',
|
||||||
|
'ClearLyngsatCacheView',
|
||||||
|
# Source
|
||||||
|
'SourceListView',
|
||||||
|
# Map
|
||||||
|
'ShowMapView',
|
||||||
|
'ShowSelectedObjectsMapView',
|
||||||
|
'ClusterTestView',
|
||||||
|
]
|
||||||
301
dbapp/mainapp/views/api.py
Normal file
301
dbapp/mainapp/views/api.py
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
"""
|
||||||
|
API endpoints for AJAX requests and data retrieval.
|
||||||
|
"""
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from ..models import ObjItem
|
||||||
|
|
||||||
|
|
||||||
|
class GetLocationsView(LoginRequiredMixin, View):
|
||||||
|
"""API endpoint for getting locations by satellite ID in GeoJSON format."""
|
||||||
|
|
||||||
|
def get(self, request, sat_id):
|
||||||
|
locations = (
|
||||||
|
ObjItem.objects.filter(parameter_obj__id_satellite=sat_id)
|
||||||
|
.select_related(
|
||||||
|
"geo_obj",
|
||||||
|
"parameter_obj",
|
||||||
|
"parameter_obj__polarization",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not locations.exists():
|
||||||
|
return JsonResponse({"error": "Объектов не найдено"}, status=404)
|
||||||
|
|
||||||
|
features = []
|
||||||
|
for loc in locations:
|
||||||
|
if not hasattr(loc, "geo_obj") or not loc.geo_obj or not loc.geo_obj.coords:
|
||||||
|
continue
|
||||||
|
|
||||||
|
param = getattr(loc, 'parameter_obj', None)
|
||||||
|
if not param:
|
||||||
|
continue
|
||||||
|
|
||||||
|
features.append(
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [loc.geo_obj.coords[0], loc.geo_obj.coords[1]],
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"pol": param.polarization.name if param.polarization else "-",
|
||||||
|
"freq": param.frequency * 1000000 if param.frequency else 0,
|
||||||
|
"name": loc.name or "-",
|
||||||
|
"id": loc.geo_obj.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({"type": "FeatureCollection", "features": features})
|
||||||
|
|
||||||
|
|
||||||
|
class LyngsatDataAPIView(LoginRequiredMixin, View):
|
||||||
|
"""API endpoint for getting LyngSat source data."""
|
||||||
|
|
||||||
|
def get(self, request, lyngsat_id):
|
||||||
|
from lyngsatapp.models import LyngSat
|
||||||
|
|
||||||
|
try:
|
||||||
|
lyngsat = LyngSat.objects.select_related(
|
||||||
|
'id_satellite',
|
||||||
|
'polarization',
|
||||||
|
'modulation',
|
||||||
|
'standard'
|
||||||
|
).get(id=lyngsat_id)
|
||||||
|
|
||||||
|
# Format date with local timezone
|
||||||
|
last_update_str = '-'
|
||||||
|
if lyngsat.last_update:
|
||||||
|
local_time = timezone.localtime(lyngsat.last_update)
|
||||||
|
last_update_str = local_time.strftime("%d.%m.%Y")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'id': lyngsat.id,
|
||||||
|
'satellite': lyngsat.id_satellite.name if lyngsat.id_satellite else '-',
|
||||||
|
'frequency': f"{lyngsat.frequency:.3f}" if lyngsat.frequency else '-',
|
||||||
|
'polarization': lyngsat.polarization.name if lyngsat.polarization else '-',
|
||||||
|
'modulation': lyngsat.modulation.name if lyngsat.modulation else '-',
|
||||||
|
'standard': lyngsat.standard.name if lyngsat.standard else '-',
|
||||||
|
'sym_velocity': f"{lyngsat.sym_velocity:.0f}" if lyngsat.sym_velocity else '-',
|
||||||
|
'fec': lyngsat.fec or '-',
|
||||||
|
'channel_info': lyngsat.channel_info or '-',
|
||||||
|
'last_update': last_update_str,
|
||||||
|
'url': lyngsat.url or None,
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponse(data)
|
||||||
|
except LyngSat.DoesNotExist:
|
||||||
|
return JsonResponse({'error': 'Источник LyngSat не найден'}, status=404)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class SigmaParameterDataAPIView(LoginRequiredMixin, View):
|
||||||
|
"""API endpoint for getting SigmaParameter data."""
|
||||||
|
|
||||||
|
def get(self, request, parameter_id):
|
||||||
|
from ..models import Parameter
|
||||||
|
|
||||||
|
try:
|
||||||
|
parameter = Parameter.objects.select_related(
|
||||||
|
'id_satellite',
|
||||||
|
'polarization',
|
||||||
|
'modulation',
|
||||||
|
'standard'
|
||||||
|
).prefetch_related(
|
||||||
|
'sigma_parameter__mark',
|
||||||
|
'sigma_parameter__id_satellite',
|
||||||
|
'sigma_parameter__polarization',
|
||||||
|
'sigma_parameter__modulation',
|
||||||
|
'sigma_parameter__standard'
|
||||||
|
).get(id=parameter_id)
|
||||||
|
|
||||||
|
# Get all related SigmaParameter
|
||||||
|
sigma_params = parameter.sigma_parameter.all()
|
||||||
|
|
||||||
|
sigma_data = []
|
||||||
|
for sigma in sigma_params:
|
||||||
|
# Get marks
|
||||||
|
marks = []
|
||||||
|
for mark in sigma.mark.all().order_by('-timestamp'):
|
||||||
|
mark_str = '+' if mark.mark else '-'
|
||||||
|
date_str = '-'
|
||||||
|
if mark.timestamp:
|
||||||
|
local_time = timezone.localtime(mark.timestamp)
|
||||||
|
date_str = local_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
marks.append({
|
||||||
|
'mark': mark_str,
|
||||||
|
'date': date_str
|
||||||
|
})
|
||||||
|
|
||||||
|
# Format start and end dates
|
||||||
|
datetime_begin_str = '-'
|
||||||
|
if sigma.datetime_begin:
|
||||||
|
local_time = timezone.localtime(sigma.datetime_begin)
|
||||||
|
datetime_begin_str = local_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
datetime_end_str = '-'
|
||||||
|
if sigma.datetime_end:
|
||||||
|
local_time = timezone.localtime(sigma.datetime_end)
|
||||||
|
datetime_end_str = local_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
sigma_data.append({
|
||||||
|
'id': sigma.id,
|
||||||
|
'satellite': sigma.id_satellite.name if sigma.id_satellite else '-',
|
||||||
|
'frequency': f"{sigma.frequency:.3f}" if sigma.frequency else '-',
|
||||||
|
'transfer_frequency': f"{sigma.transfer_frequency:.3f}" if sigma.transfer_frequency else '-',
|
||||||
|
'freq_range': f"{sigma.freq_range:.3f}" if sigma.freq_range else '-',
|
||||||
|
'polarization': sigma.polarization.name if sigma.polarization else '-',
|
||||||
|
'modulation': sigma.modulation.name if sigma.modulation else '-',
|
||||||
|
'standard': sigma.standard.name if sigma.standard else '-',
|
||||||
|
'bod_velocity': f"{sigma.bod_velocity:.0f}" if sigma.bod_velocity else '-',
|
||||||
|
'snr': f"{sigma.snr:.1f}" if sigma.snr is not None else '-',
|
||||||
|
'power': f"{sigma.power:.1f}" if sigma.power is not None else '-',
|
||||||
|
'status': sigma.status or '-',
|
||||||
|
'packets': 'Да' if sigma.packets else 'Нет' if sigma.packets is not None else '-',
|
||||||
|
'datetime_begin': datetime_begin_str,
|
||||||
|
'datetime_end': datetime_end_str,
|
||||||
|
'marks': marks
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'parameter_id': parameter.id,
|
||||||
|
'sigma_parameters': sigma_data
|
||||||
|
})
|
||||||
|
except Parameter.DoesNotExist:
|
||||||
|
return JsonResponse({'error': 'Parameter не найден'}, status=404)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
||||||
|
"""API endpoint for getting ObjItems related to a Source."""
|
||||||
|
|
||||||
|
def get(self, request, source_id):
|
||||||
|
from ..models import Source
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load Source with prefetch_related for ObjItem
|
||||||
|
source = Source.objects.prefetch_related(
|
||||||
|
'source_objitems',
|
||||||
|
'source_objitems__parameter_obj',
|
||||||
|
'source_objitems__parameter_obj__id_satellite',
|
||||||
|
'source_objitems__parameter_obj__polarization',
|
||||||
|
'source_objitems__parameter_obj__modulation',
|
||||||
|
'source_objitems__geo_obj'
|
||||||
|
).get(id=source_id)
|
||||||
|
|
||||||
|
# Get all related ObjItems, sorted by created_at
|
||||||
|
objitems = source.source_objitems.all().order_by('created_at')
|
||||||
|
|
||||||
|
objitems_data = []
|
||||||
|
for objitem in objitems:
|
||||||
|
# Get parameter data
|
||||||
|
param = getattr(objitem, 'parameter_obj', None)
|
||||||
|
satellite_name = '-'
|
||||||
|
frequency = '-'
|
||||||
|
freq_range = '-'
|
||||||
|
polarization = '-'
|
||||||
|
bod_velocity = '-'
|
||||||
|
modulation = '-'
|
||||||
|
snr = '-'
|
||||||
|
|
||||||
|
if param:
|
||||||
|
if hasattr(param, 'id_satellite') and param.id_satellite:
|
||||||
|
satellite_name = param.id_satellite.name
|
||||||
|
frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-'
|
||||||
|
freq_range = f"{param.freq_range:.3f}" if param.freq_range is not None else '-'
|
||||||
|
if hasattr(param, 'polarization') and param.polarization:
|
||||||
|
polarization = param.polarization.name
|
||||||
|
bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else '-'
|
||||||
|
if hasattr(param, 'modulation') and param.modulation:
|
||||||
|
modulation = param.modulation.name
|
||||||
|
snr = f"{param.snr:.0f}" if param.snr is not None else '-'
|
||||||
|
|
||||||
|
# Get geo data
|
||||||
|
geo_timestamp = '-'
|
||||||
|
geo_location = '-'
|
||||||
|
geo_coords = '-'
|
||||||
|
|
||||||
|
if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
|
||||||
|
if objitem.geo_obj.timestamp:
|
||||||
|
local_time = timezone.localtime(objitem.geo_obj.timestamp)
|
||||||
|
geo_timestamp = local_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
geo_location = objitem.geo_obj.location or '-'
|
||||||
|
|
||||||
|
if objitem.geo_obj.coords:
|
||||||
|
longitude = objitem.geo_obj.coords.coords[0]
|
||||||
|
latitude = objitem.geo_obj.coords.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
geo_coords = f"{lat} {lon}"
|
||||||
|
|
||||||
|
objitems_data.append({
|
||||||
|
'id': objitem.id,
|
||||||
|
'name': objitem.name or '-',
|
||||||
|
'satellite_name': satellite_name,
|
||||||
|
'frequency': frequency,
|
||||||
|
'freq_range': freq_range,
|
||||||
|
'polarization': polarization,
|
||||||
|
'bod_velocity': bod_velocity,
|
||||||
|
'modulation': modulation,
|
||||||
|
'snr': snr,
|
||||||
|
'geo_timestamp': geo_timestamp,
|
||||||
|
'geo_location': geo_location,
|
||||||
|
'geo_coords': geo_coords
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'source_id': source_id,
|
||||||
|
'objitems': objitems_data
|
||||||
|
})
|
||||||
|
except Source.DoesNotExist:
|
||||||
|
return JsonResponse({'error': 'Источник не найден'}, status=404)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
|
||||||
|
"""API endpoint for getting Celery task status."""
|
||||||
|
|
||||||
|
def get(self, request, task_id):
|
||||||
|
from celery.result import AsyncResult
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
task = AsyncResult(task_id)
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'task_id': task_id,
|
||||||
|
'state': task.state,
|
||||||
|
'result': None,
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.state == 'PENDING':
|
||||||
|
response_data['status'] = 'Задача в очереди...'
|
||||||
|
elif task.state == 'PROGRESS':
|
||||||
|
response_data['status'] = task.info.get('status', '')
|
||||||
|
response_data['current'] = task.info.get('current', 0)
|
||||||
|
response_data['total'] = task.info.get('total', 1)
|
||||||
|
response_data['percent'] = int((task.info.get('current', 0) / task.info.get('total', 1)) * 100)
|
||||||
|
elif task.state == 'SUCCESS':
|
||||||
|
# Get result from cache
|
||||||
|
result = cache.get(f'lyngsat_task_{task_id}')
|
||||||
|
if result:
|
||||||
|
response_data['result'] = result
|
||||||
|
response_data['status'] = 'Задача завершена успешно'
|
||||||
|
else:
|
||||||
|
response_data['result'] = task.result
|
||||||
|
response_data['status'] = 'Задача завершена'
|
||||||
|
elif task.state == 'FAILURE':
|
||||||
|
response_data['status'] = 'Ошибка при выполнении задачи'
|
||||||
|
response_data['error'] = str(task.info)
|
||||||
|
else:
|
||||||
|
response_data['status'] = task.state
|
||||||
|
|
||||||
|
return JsonResponse(response_data)
|
||||||
22
dbapp/mainapp/views/base.py
Normal file
22
dbapp/mainapp/views/base.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
Base views and utilities.
|
||||||
|
"""
|
||||||
|
from django.contrib.auth import logout
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsPageView(View):
|
||||||
|
"""View for displaying the actions page."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return render(request, "mainapp/actions.html")
|
||||||
|
else:
|
||||||
|
return render(request, "mainapp/login_required.html")
|
||||||
|
|
||||||
|
|
||||||
|
def custom_logout(request):
|
||||||
|
"""Custom logout view."""
|
||||||
|
logout(request)
|
||||||
|
return redirect("mainapp:home")
|
||||||
205
dbapp/mainapp/views/data_import.py
Normal file
205
dbapp/mainapp/views/data_import.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
Data import views (Excel, CSV, Transponders, VCH, etc.).
|
||||||
|
"""
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views import View
|
||||||
|
from django.views.generic import FormView
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from ..forms import (
|
||||||
|
LoadCsvData,
|
||||||
|
LoadExcelData,
|
||||||
|
NewEventForm,
|
||||||
|
UploadFileForm,
|
||||||
|
UploadVchLoad,
|
||||||
|
VchLinkForm,
|
||||||
|
)
|
||||||
|
from ..mixins import FormMessageMixin
|
||||||
|
from ..utils import (
|
||||||
|
add_satellite_list,
|
||||||
|
compare_and_link_vch_load,
|
||||||
|
fill_data_from_df,
|
||||||
|
get_points_from_csv,
|
||||||
|
get_vch_load_from_html,
|
||||||
|
kub_report,
|
||||||
|
)
|
||||||
|
from mapsapp.utils import parse_transponders_from_xml
|
||||||
|
|
||||||
|
|
||||||
|
class AddSatellitesView(LoginRequiredMixin, View):
|
||||||
|
"""View for adding satellites to the database."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
add_satellite_list()
|
||||||
|
return redirect("mainapp:home")
|
||||||
|
|
||||||
|
|
||||||
|
class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
|
"""View for uploading and parsing transponder data from XML."""
|
||||||
|
|
||||||
|
template_name = "mainapp/transponders_upload.html"
|
||||||
|
form_class = UploadFileForm
|
||||||
|
success_message = "Файл успешно обработан"
|
||||||
|
error_message = "Форма заполнена некорректно"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
uploaded_file = self.request.FILES["file"]
|
||||||
|
try:
|
||||||
|
content = uploaded_file.read()
|
||||||
|
parse_transponders_from_xml(BytesIO(content))
|
||||||
|
except ValueError as e:
|
||||||
|
messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
|
||||||
|
return redirect("mainapp:add_trans")
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(self.request, f"Неизвестная ошибка: {e}")
|
||||||
|
return redirect("mainapp:add_trans")
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("mainapp:add_trans")
|
||||||
|
|
||||||
|
|
||||||
|
class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
|
"""View for loading data from Excel files."""
|
||||||
|
|
||||||
|
template_name = "mainapp/add_data_from_excel.html"
|
||||||
|
form_class = LoadExcelData
|
||||||
|
error_message = "Форма заполнена некорректно"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
uploaded_file = self.request.FILES["file"]
|
||||||
|
selected_sat = form.cleaned_data["sat_choice"]
|
||||||
|
number = form.cleaned_data["number_input"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import io
|
||||||
|
|
||||||
|
df = pd.read_excel(io.BytesIO(uploaded_file.read()))
|
||||||
|
if number > 0:
|
||||||
|
df = df.head(number)
|
||||||
|
result = fill_data_from_df(df, selected_sat, self.request.user.customuser)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
self.request, f"Данные успешно загружены! Обработано строк: {result}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
|
||||||
|
|
||||||
|
return redirect("mainapp:load_excel_data")
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("mainapp:load_excel_data")
|
||||||
|
|
||||||
|
|
||||||
|
class LoadCsvDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
|
"""View for loading data from CSV files."""
|
||||||
|
|
||||||
|
template_name = "mainapp/add_data_from_csv.html"
|
||||||
|
form_class = LoadCsvData
|
||||||
|
success_message = "Данные успешно загружены!"
|
||||||
|
error_message = "Форма заполнена некорректно"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
uploaded_file = self.request.FILES["file"]
|
||||||
|
try:
|
||||||
|
content = uploaded_file.read()
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
content = content.decode("utf-8")
|
||||||
|
|
||||||
|
get_points_from_csv(content, self.request.user.customuser)
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
|
||||||
|
return redirect("mainapp:load_csv_data")
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("mainapp:load_csv_data")
|
||||||
|
|
||||||
|
|
||||||
|
class UploadVchLoadView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
|
"""View for uploading VCH load data from HTML files."""
|
||||||
|
|
||||||
|
template_name = "mainapp/upload_html.html"
|
||||||
|
form_class = UploadVchLoad
|
||||||
|
success_message = "Файл успешно обработан"
|
||||||
|
error_message = "Форма заполнена некорректно"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
selected_sat = form.cleaned_data["sat_choice"]
|
||||||
|
uploaded_file = self.request.FILES["file"]
|
||||||
|
try:
|
||||||
|
get_vch_load_from_html(uploaded_file, selected_sat)
|
||||||
|
except ValueError as e:
|
||||||
|
messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
|
||||||
|
return redirect("mainapp:vch_load")
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(self.request, f"Неизвестная ошибка: {e}")
|
||||||
|
return redirect("mainapp:vch_load")
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("mainapp:vch_load")
|
||||||
|
|
||||||
|
|
||||||
|
class LinkVchSigmaView(LoginRequiredMixin, FormView):
|
||||||
|
"""View for linking VCH data with Sigma parameters."""
|
||||||
|
|
||||||
|
template_name = "mainapp/link_vch.html"
|
||||||
|
form_class = VchLinkForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# value1 is no longer used - frequency tolerance is determined automatically
|
||||||
|
freq_range = form.cleaned_data["value2"]
|
||||||
|
sat_id = form.cleaned_data["sat_choice"]
|
||||||
|
|
||||||
|
# Pass 0 for eps_freq and ku_range as they are not used
|
||||||
|
count_all, link_count = compare_and_link_vch_load(sat_id, 0, freq_range, 0)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
self.request, f"Привязано {link_count} из {count_all} объектов"
|
||||||
|
)
|
||||||
|
return redirect("mainapp:link_vch_sigma")
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
return self.render_to_response(self.get_context_data(form=form))
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
|
"""View for processing Kubsat event data."""
|
||||||
|
|
||||||
|
template_name = "mainapp/process_kubsat.html"
|
||||||
|
form_class = NewEventForm
|
||||||
|
error_message = "Форма заполнена некорректно"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
uploaded_file = self.request.FILES["file"]
|
||||||
|
try:
|
||||||
|
content = uploaded_file.read()
|
||||||
|
df = kub_report(BytesIO(content))
|
||||||
|
output = BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
||||||
|
df.to_excel(writer, index=False, sheet_name="Результат")
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
response = HttpResponse(
|
||||||
|
output.getvalue(),
|
||||||
|
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
)
|
||||||
|
response["Content-Disposition"] = (
|
||||||
|
'attachment; filename="kubsat_report.xlsx"'
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.success(self.request, "Событие успешно обработано!")
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
|
||||||
|
return redirect("mainapp:kubsat_excel")
|
||||||
161
dbapp/mainapp/views/lyngsat.py
Normal file
161
dbapp/mainapp/views/lyngsat.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
"""
|
||||||
|
LyngSat related views for data synchronization and linking.
|
||||||
|
"""
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views import View
|
||||||
|
from django.views.generic import FormView
|
||||||
|
|
||||||
|
from ..forms import FillLyngsatDataForm, LinkLyngsatForm
|
||||||
|
from ..mixins import FormMessageMixin
|
||||||
|
from ..models import ObjItem
|
||||||
|
|
||||||
|
|
||||||
|
class LinkLyngsatSourcesView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
|
"""View for linking LyngSat sources to objects."""
|
||||||
|
|
||||||
|
template_name = "mainapp/link_lyngsat.html"
|
||||||
|
form_class = LinkLyngsatForm
|
||||||
|
success_message = "Привязка источников LyngSat завершена"
|
||||||
|
error_message = "Ошибка при привязке источников"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
from lyngsatapp.models import LyngSat
|
||||||
|
|
||||||
|
satellites = form.cleaned_data.get("satellites")
|
||||||
|
frequency_tolerance = form.cleaned_data.get("frequency_tolerance", 0.5)
|
||||||
|
|
||||||
|
# If satellites not selected, process all
|
||||||
|
if satellites:
|
||||||
|
objitems = ObjItem.objects.filter(
|
||||||
|
parameter_obj__id_satellite__in=satellites
|
||||||
|
).select_related('parameter_obj', 'parameter_obj__polarization')
|
||||||
|
else:
|
||||||
|
objitems = ObjItem.objects.filter(
|
||||||
|
parameter_obj__isnull=False
|
||||||
|
).select_related('parameter_obj', 'parameter_obj__polarization')
|
||||||
|
|
||||||
|
linked_count = 0
|
||||||
|
total_count = objitems.count()
|
||||||
|
|
||||||
|
for objitem in objitems:
|
||||||
|
if not hasattr(objitem, 'parameter_obj') or not objitem.parameter_obj:
|
||||||
|
continue
|
||||||
|
|
||||||
|
param = objitem.parameter_obj
|
||||||
|
|
||||||
|
# Round object frequency
|
||||||
|
if param.frequency:
|
||||||
|
rounded_freq = round(param.frequency, 0) # Round to integer
|
||||||
|
|
||||||
|
# Find matching LyngSat source
|
||||||
|
# Compare by rounded frequency and polarization
|
||||||
|
lyngsat_sources = LyngSat.objects.filter(
|
||||||
|
id_satellite=param.id_satellite,
|
||||||
|
polarization=param.polarization,
|
||||||
|
frequency__gte=rounded_freq - frequency_tolerance,
|
||||||
|
frequency__lte=rounded_freq + frequency_tolerance
|
||||||
|
).order_by('frequency')
|
||||||
|
|
||||||
|
if lyngsat_sources.exists():
|
||||||
|
# Take first matching source
|
||||||
|
objitem.lyngsat_source = lyngsat_sources.first()
|
||||||
|
objitem.save(update_fields=['lyngsat_source'])
|
||||||
|
linked_count += 1
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
self.request,
|
||||||
|
f"Привязано {linked_count} из {total_count} объектов к источникам LyngSat"
|
||||||
|
)
|
||||||
|
return redirect("mainapp:link_lyngsat")
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
return self.render_to_response(self.get_context_data(form=form))
|
||||||
|
|
||||||
|
|
||||||
|
class FillLyngsatDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
|
"""
|
||||||
|
View for filling data from Lyngsat.
|
||||||
|
|
||||||
|
Allows selecting satellites and regions for parsing data from Lyngsat website.
|
||||||
|
Starts asynchronous Celery task for processing.
|
||||||
|
"""
|
||||||
|
template_name = "mainapp/fill_lyngsat_data.html"
|
||||||
|
form_class = FillLyngsatDataForm
|
||||||
|
success_url = reverse_lazy("mainapp:lyngsat_task_status")
|
||||||
|
error_message = "Форма заполнена некорректно"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
satellites = form.cleaned_data["satellites"]
|
||||||
|
regions = form.cleaned_data["regions"]
|
||||||
|
use_cache = form.cleaned_data.get("use_cache", True)
|
||||||
|
force_refresh = form.cleaned_data.get("force_refresh", False)
|
||||||
|
|
||||||
|
# Get satellite names
|
||||||
|
target_sats = [sat.name for sat in satellites]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from lyngsatapp.tasks import fill_lyngsat_data_task
|
||||||
|
|
||||||
|
# Start asynchronous task with caching parameters
|
||||||
|
task = fill_lyngsat_data_task.delay(
|
||||||
|
target_sats,
|
||||||
|
regions,
|
||||||
|
force_refresh=force_refresh,
|
||||||
|
use_cache=use_cache
|
||||||
|
)
|
||||||
|
|
||||||
|
cache_status = "без кеша" if not use_cache else ("с обновлением кеша" if force_refresh else "с кешированием")
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
self.request,
|
||||||
|
f"Задача запущена ({cache_status})! ID задачи: {task.id}. "
|
||||||
|
"Вы будете перенаправлены на страницу отслеживания прогресса."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Redirect to task status page
|
||||||
|
return redirect('mainapp:lyngsat_task_status', task_id=task.id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(self.request, f"Ошибка при запуске задачи: {str(e)}")
|
||||||
|
return redirect("mainapp:fill_lyngsat_data")
|
||||||
|
|
||||||
|
|
||||||
|
class LyngsatTaskStatusView(LoginRequiredMixin, View):
|
||||||
|
"""View for tracking Lyngsat data filling task status."""
|
||||||
|
|
||||||
|
template_name = "mainapp/lyngsat_task_status.html"
|
||||||
|
|
||||||
|
def get(self, request, task_id=None):
|
||||||
|
context = {
|
||||||
|
'task_id': task_id
|
||||||
|
}
|
||||||
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
|
|
||||||
|
class ClearLyngsatCacheView(LoginRequiredMixin, View):
|
||||||
|
"""View for clearing LyngSat cache."""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from lyngsatapp.tasks import clear_cache_task
|
||||||
|
|
||||||
|
cache_type = request.POST.get('cache_type', 'all')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Start cache clearing task
|
||||||
|
task = clear_cache_task.delay(cache_type)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Задача очистки кеша ({cache_type}) запущена! ID задачи: {task.id}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"Ошибка при запуске задачи очистки кеша: {str(e)}")
|
||||||
|
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', 'mainapp:home'))
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""Cache management page."""
|
||||||
|
return render(request, 'mainapp/clear_lyngsat_cache.html')
|
||||||
139
dbapp/mainapp/views/map.py
Normal file
139
dbapp/mainapp/views/map.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
Map related views for displaying objects on maps.
|
||||||
|
"""
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from ..clusters import get_clusters
|
||||||
|
from ..mixins import RoleRequiredMixin
|
||||||
|
from ..models import ObjItem
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(staff_member_required, name="dispatch")
|
||||||
|
class ShowMapView(RoleRequiredMixin, View):
|
||||||
|
"""View for displaying objects on map (admin interface)."""
|
||||||
|
|
||||||
|
required_roles = ["admin", "moderator"]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
ids = request.GET.get("ids", "")
|
||||||
|
points = []
|
||||||
|
if ids:
|
||||||
|
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||||
|
locations = ObjItem.objects.filter(id__in=id_list).select_related(
|
||||||
|
"parameter_obj",
|
||||||
|
"parameter_obj__id_satellite",
|
||||||
|
"parameter_obj__polarization",
|
||||||
|
"parameter_obj__modulation",
|
||||||
|
"parameter_obj__standard",
|
||||||
|
"geo_obj",
|
||||||
|
)
|
||||||
|
for obj in locations:
|
||||||
|
if (
|
||||||
|
not hasattr(obj, "geo_obj")
|
||||||
|
or not obj.geo_obj
|
||||||
|
or not obj.geo_obj.coords
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
param = getattr(obj, 'parameter_obj', None)
|
||||||
|
if not param:
|
||||||
|
continue
|
||||||
|
points.append(
|
||||||
|
{
|
||||||
|
"name": f"{obj.name}",
|
||||||
|
"freq": f"{param.frequency} [{param.freq_range}] МГц",
|
||||||
|
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return redirect("admin")
|
||||||
|
|
||||||
|
grouped = defaultdict(list)
|
||||||
|
for p in points:
|
||||||
|
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
|
||||||
|
|
||||||
|
groups = [
|
||||||
|
{"name": name, "points": coords_list}
|
||||||
|
for name, coords_list in grouped.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"groups": groups,
|
||||||
|
}
|
||||||
|
return render(request, "admin/map_custom.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
||||||
|
"""View for displaying selected objects on map."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
ids = request.GET.get("ids", "")
|
||||||
|
points = []
|
||||||
|
if ids:
|
||||||
|
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||||
|
locations = ObjItem.objects.filter(id__in=id_list).select_related(
|
||||||
|
"parameter_obj",
|
||||||
|
"parameter_obj__id_satellite",
|
||||||
|
"parameter_obj__polarization",
|
||||||
|
"parameter_obj__modulation",
|
||||||
|
"parameter_obj__standard",
|
||||||
|
"geo_obj",
|
||||||
|
)
|
||||||
|
for obj in locations:
|
||||||
|
if (
|
||||||
|
not hasattr(obj, "geo_obj")
|
||||||
|
or not obj.geo_obj
|
||||||
|
or not obj.geo_obj.coords
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
param = getattr(obj, 'parameter_obj', None)
|
||||||
|
if not param:
|
||||||
|
continue
|
||||||
|
points.append(
|
||||||
|
{
|
||||||
|
"name": f"{obj.name}",
|
||||||
|
"freq": f"{param.frequency} [{param.freq_range}] МГц",
|
||||||
|
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return redirect("mainapp:objitem_list")
|
||||||
|
|
||||||
|
# Group points by object name
|
||||||
|
grouped = defaultdict(list)
|
||||||
|
for p in points:
|
||||||
|
grouped[p["name"]].append({"point": p["point"], "frequency": p["freq"]})
|
||||||
|
|
||||||
|
groups = [
|
||||||
|
{"name": name, "points": coords_list}
|
||||||
|
for name, coords_list in grouped.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"groups": groups,
|
||||||
|
}
|
||||||
|
return render(request, "mainapp/objitem_map.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterTestView(LoginRequiredMixin, View):
|
||||||
|
"""Test view for clustering functionality."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
objs = ObjItem.objects.filter(
|
||||||
|
name__icontains="! Astra 4A 12654,040 [1,962] МГц H"
|
||||||
|
)
|
||||||
|
coords = []
|
||||||
|
for obj in objs:
|
||||||
|
if hasattr(obj, "geo_obj") and obj.geo_obj and obj.geo_obj.coords:
|
||||||
|
coords.append(
|
||||||
|
(obj.geo_obj.coords.coords[1], obj.geo_obj.coords.coords[0])
|
||||||
|
)
|
||||||
|
get_clusters(coords)
|
||||||
|
|
||||||
|
return JsonResponse({"success": "ок"})
|
||||||
666
dbapp/mainapp/views/objitem.py
Normal file
666
dbapp/mainapp/views/objitem.py
Normal file
@@ -0,0 +1,666 @@
|
|||||||
|
"""
|
||||||
|
ObjItem CRUD operations and related views.
|
||||||
|
"""
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import F
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views import View
|
||||||
|
from django.views.generic import CreateView, DeleteView, UpdateView
|
||||||
|
|
||||||
|
from ..forms import GeoForm, ObjItemForm, ParameterForm
|
||||||
|
from ..mixins import CoordinateProcessingMixin, FormMessageMixin, RoleRequiredMixin
|
||||||
|
from ..models import Geo, Modulation, ObjItem, Polarization, Satellite
|
||||||
|
from ..utils import parse_pagination_params
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteSelectedObjectsView(RoleRequiredMixin, View):
|
||||||
|
"""View for deleting multiple selected objects."""
|
||||||
|
|
||||||
|
required_roles = ["admin", "moderator"]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
ids = request.POST.get("ids", "")
|
||||||
|
if not ids:
|
||||||
|
return JsonResponse({"error": "Нет ID для удаления"}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||||
|
deleted_count, _ = ObjItem.objects.filter(id__in=id_list).delete()
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": "Объект успешно удалён",
|
||||||
|
"deleted_count": deleted_count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({"error": f"Ошибка при удалении: {str(e)}"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemListView(LoginRequiredMixin, View):
|
||||||
|
"""View for displaying a list of ObjItems with filtering and pagination."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
satellites = (
|
||||||
|
Satellite.objects.filter(parameters__objitem__isnull=False)
|
||||||
|
.distinct()
|
||||||
|
.only("id", "name")
|
||||||
|
.order_by("name")
|
||||||
|
)
|
||||||
|
|
||||||
|
selected_sat_id = request.GET.get("satellite_id")
|
||||||
|
page_number, items_per_page = parse_pagination_params(request)
|
||||||
|
sort_param = request.GET.get("sort", "")
|
||||||
|
|
||||||
|
freq_min = request.GET.get("freq_min")
|
||||||
|
freq_max = request.GET.get("freq_max")
|
||||||
|
range_min = request.GET.get("range_min")
|
||||||
|
range_max = request.GET.get("range_max")
|
||||||
|
snr_min = request.GET.get("snr_min")
|
||||||
|
snr_max = request.GET.get("snr_max")
|
||||||
|
bod_min = request.GET.get("bod_min")
|
||||||
|
bod_max = request.GET.get("bod_max")
|
||||||
|
search_query = request.GET.get("search")
|
||||||
|
selected_modulations = request.GET.getlist("modulation")
|
||||||
|
selected_polarizations = request.GET.getlist("polarization")
|
||||||
|
selected_satellites = request.GET.getlist("satellite_id")
|
||||||
|
has_kupsat = request.GET.get("has_kupsat")
|
||||||
|
has_valid = request.GET.get("has_valid")
|
||||||
|
date_from = request.GET.get("date_from")
|
||||||
|
date_to = request.GET.get("date_to")
|
||||||
|
|
||||||
|
objects = ObjItem.objects.none()
|
||||||
|
|
||||||
|
if selected_satellites or selected_sat_id:
|
||||||
|
if selected_sat_id and not selected_satellites:
|
||||||
|
try:
|
||||||
|
selected_sat_id_single = int(selected_sat_id)
|
||||||
|
selected_satellites = [selected_sat_id_single]
|
||||||
|
except ValueError:
|
||||||
|
selected_satellites = []
|
||||||
|
|
||||||
|
if selected_satellites:
|
||||||
|
objects = (
|
||||||
|
ObjItem.objects.select_related(
|
||||||
|
"geo_obj",
|
||||||
|
"source",
|
||||||
|
"updated_by__user",
|
||||||
|
"created_by__user",
|
||||||
|
"lyngsat_source",
|
||||||
|
"parameter_obj",
|
||||||
|
"parameter_obj__id_satellite",
|
||||||
|
"parameter_obj__polarization",
|
||||||
|
"parameter_obj__modulation",
|
||||||
|
"parameter_obj__standard",
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
"parameter_obj__sigma_parameter",
|
||||||
|
"parameter_obj__sigma_parameter__polarization",
|
||||||
|
)
|
||||||
|
.filter(parameter_obj__id_satellite_id__in=selected_satellites)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
objects = ObjItem.objects.select_related(
|
||||||
|
"geo_obj",
|
||||||
|
"source",
|
||||||
|
"updated_by__user",
|
||||||
|
"created_by__user",
|
||||||
|
"lyngsat_source",
|
||||||
|
"parameter_obj",
|
||||||
|
"parameter_obj__id_satellite",
|
||||||
|
"parameter_obj__polarization",
|
||||||
|
"parameter_obj__modulation",
|
||||||
|
"parameter_obj__standard",
|
||||||
|
).prefetch_related(
|
||||||
|
"parameter_obj__sigma_parameter",
|
||||||
|
"parameter_obj__sigma_parameter__polarization",
|
||||||
|
)
|
||||||
|
|
||||||
|
if freq_min is not None and freq_min.strip() != "":
|
||||||
|
try:
|
||||||
|
freq_min_val = float(freq_min)
|
||||||
|
objects = objects.filter(
|
||||||
|
parameter_obj__frequency__gte=freq_min_val
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if freq_max is not None and freq_max.strip() != "":
|
||||||
|
try:
|
||||||
|
freq_max_val = float(freq_max)
|
||||||
|
objects = objects.filter(
|
||||||
|
parameter_obj__frequency__lte=freq_max_val
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if range_min is not None and range_min.strip() != "":
|
||||||
|
try:
|
||||||
|
range_min_val = float(range_min)
|
||||||
|
objects = objects.filter(
|
||||||
|
parameter_obj__freq_range__gte=range_min_val
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if range_max is not None and range_max.strip() != "":
|
||||||
|
try:
|
||||||
|
range_max_val = float(range_max)
|
||||||
|
objects = objects.filter(
|
||||||
|
parameter_obj__freq_range__lte=range_max_val
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if snr_min is not None and snr_min.strip() != "":
|
||||||
|
try:
|
||||||
|
snr_min_val = float(snr_min)
|
||||||
|
objects = objects.filter(parameter_obj__snr__gte=snr_min_val)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if snr_max is not None and snr_max.strip() != "":
|
||||||
|
try:
|
||||||
|
snr_max_val = float(snr_max)
|
||||||
|
objects = objects.filter(parameter_obj__snr__lte=snr_max_val)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if bod_min is not None and bod_min.strip() != "":
|
||||||
|
try:
|
||||||
|
bod_min_val = float(bod_min)
|
||||||
|
objects = objects.filter(
|
||||||
|
parameter_obj__bod_velocity__gte=bod_min_val
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if bod_max is not None and bod_max.strip() != "":
|
||||||
|
try:
|
||||||
|
bod_max_val = float(bod_max)
|
||||||
|
objects = objects.filter(
|
||||||
|
parameter_obj__bod_velocity__lte=bod_max_val
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if selected_modulations:
|
||||||
|
objects = objects.filter(
|
||||||
|
parameter_obj__modulation__id__in=selected_modulations
|
||||||
|
)
|
||||||
|
|
||||||
|
if selected_polarizations:
|
||||||
|
objects = objects.filter(
|
||||||
|
parameter_obj__polarization__id__in=selected_polarizations
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_kupsat == "1":
|
||||||
|
objects = objects.filter(source__coords_kupsat__isnull=False)
|
||||||
|
elif has_kupsat == "0":
|
||||||
|
objects = objects.filter(source__coords_kupsat__isnull=True)
|
||||||
|
|
||||||
|
if has_valid == "1":
|
||||||
|
objects = objects.filter(source__coords_valid__isnull=False)
|
||||||
|
elif has_valid == "0":
|
||||||
|
objects = objects.filter(source__coords_valid__isnull=True)
|
||||||
|
|
||||||
|
# Date filter for geo_obj timestamp
|
||||||
|
if date_from and date_from.strip():
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
|
||||||
|
objects = objects.filter(geo_obj__timestamp__gte=date_from_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if date_to and date_to.strip():
|
||||||
|
try:
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d")
|
||||||
|
# Add one day to include the entire end date
|
||||||
|
date_to_obj = date_to_obj + timedelta(days=1)
|
||||||
|
objects = objects.filter(geo_obj__timestamp__lt=date_to_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter by source type (lyngsat_source)
|
||||||
|
has_source_type = request.GET.get("has_source_type")
|
||||||
|
if has_source_type == "1":
|
||||||
|
objects = objects.filter(lyngsat_source__isnull=False)
|
||||||
|
elif has_source_type == "0":
|
||||||
|
objects = objects.filter(lyngsat_source__isnull=True)
|
||||||
|
|
||||||
|
# Filter by sigma (sigma parameters)
|
||||||
|
has_sigma = request.GET.get("has_sigma")
|
||||||
|
if has_sigma == "1":
|
||||||
|
objects = objects.filter(parameter_obj__sigma_parameter__isnull=False)
|
||||||
|
elif has_sigma == "0":
|
||||||
|
objects = objects.filter(parameter_obj__sigma_parameter__isnull=True)
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
search_query = search_query.strip()
|
||||||
|
if search_query:
|
||||||
|
objects = objects.filter(
|
||||||
|
models.Q(name__icontains=search_query)
|
||||||
|
| models.Q(geo_obj__location__icontains=search_query)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
selected_sat_id = None
|
||||||
|
|
||||||
|
objects = objects.annotate(
|
||||||
|
first_param_freq=F("parameter_obj__frequency"),
|
||||||
|
first_param_range=F("parameter_obj__freq_range"),
|
||||||
|
first_param_snr=F("parameter_obj__snr"),
|
||||||
|
first_param_bod=F("parameter_obj__bod_velocity"),
|
||||||
|
first_param_sat_name=F("parameter_obj__id_satellite__name"),
|
||||||
|
first_param_pol_name=F("parameter_obj__polarization__name"),
|
||||||
|
first_param_mod_name=F("parameter_obj__modulation__name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_sort_fields = {
|
||||||
|
"name": "name",
|
||||||
|
"-name": "-name",
|
||||||
|
"updated_at": "updated_at",
|
||||||
|
"-updated_at": "-updated_at",
|
||||||
|
"created_at": "created_at",
|
||||||
|
"-created_at": "-created_at",
|
||||||
|
"updated_by": "updated_by__user__username",
|
||||||
|
"-updated_by": "-updated_by__user__username",
|
||||||
|
"created_by": "created_by__user__username",
|
||||||
|
"-created_by": "-created_by__user__username",
|
||||||
|
"geo_timestamp": "geo_obj__timestamp",
|
||||||
|
"-geo_timestamp": "-geo_obj__timestamp",
|
||||||
|
"frequency": "first_param_freq",
|
||||||
|
"-frequency": "-first_param_freq",
|
||||||
|
"freq_range": "first_param_range",
|
||||||
|
"-freq_range": "-first_param_range",
|
||||||
|
"snr": "first_param_snr",
|
||||||
|
"-snr": "-first_param_snr",
|
||||||
|
"bod_velocity": "first_param_bod",
|
||||||
|
"-bod_velocity": "-first_param_bod",
|
||||||
|
"satellite": "first_param_sat_name",
|
||||||
|
"-satellite": "-first_param_sat_name",
|
||||||
|
"polarization": "first_param_pol_name",
|
||||||
|
"-polarization": "-first_param_pol_name",
|
||||||
|
"modulation": "first_param_mod_name",
|
||||||
|
"-modulation": "-first_param_mod_name",
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort_param in valid_sort_fields:
|
||||||
|
objects = objects.order_by(valid_sort_fields[sort_param])
|
||||||
|
|
||||||
|
paginator = Paginator(objects, items_per_page)
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
processed_objects = []
|
||||||
|
for obj in page_obj:
|
||||||
|
param = getattr(obj, 'parameter_obj', None)
|
||||||
|
|
||||||
|
geo_coords = "-"
|
||||||
|
geo_timestamp = "-"
|
||||||
|
geo_location = "-"
|
||||||
|
kupsat_coords = "-"
|
||||||
|
valid_coords = "-"
|
||||||
|
distance_geo_kup = "-"
|
||||||
|
distance_geo_valid = "-"
|
||||||
|
distance_kup_valid = "-"
|
||||||
|
|
||||||
|
if hasattr(obj, "geo_obj") and obj.geo_obj:
|
||||||
|
geo_timestamp = obj.geo_obj.timestamp
|
||||||
|
geo_location = obj.geo_obj.location
|
||||||
|
|
||||||
|
if obj.geo_obj.coords:
|
||||||
|
longitude = obj.geo_obj.coords.coords[0]
|
||||||
|
latitude = obj.geo_obj.coords.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
geo_coords = f"{lat} {lon}"
|
||||||
|
|
||||||
|
satellite_name = "-"
|
||||||
|
frequency = "-"
|
||||||
|
freq_range = "-"
|
||||||
|
polarization_name = "-"
|
||||||
|
bod_velocity = "-"
|
||||||
|
modulation_name = "-"
|
||||||
|
snr = "-"
|
||||||
|
standard_name = "-"
|
||||||
|
comment = "-"
|
||||||
|
is_average = "-"
|
||||||
|
|
||||||
|
if param:
|
||||||
|
if hasattr(param, "id_satellite") and param.id_satellite:
|
||||||
|
satellite_name = (
|
||||||
|
param.id_satellite.name
|
||||||
|
if hasattr(param.id_satellite, "name")
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
frequency = (
|
||||||
|
f"{param.frequency:.3f}" if param.frequency is not None else "-"
|
||||||
|
)
|
||||||
|
freq_range = (
|
||||||
|
f"{param.freq_range:.3f}" if param.freq_range is not None else "-"
|
||||||
|
)
|
||||||
|
bod_velocity = (
|
||||||
|
f"{param.bod_velocity:.0f}"
|
||||||
|
if param.bod_velocity is not None
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
snr = f"{param.snr:.0f}" if param.snr is not None else "-"
|
||||||
|
|
||||||
|
if hasattr(param, "polarization") and param.polarization:
|
||||||
|
polarization_name = (
|
||||||
|
param.polarization.name
|
||||||
|
if hasattr(param.polarization, "name")
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(param, "modulation") and param.modulation:
|
||||||
|
modulation_name = (
|
||||||
|
param.modulation.name
|
||||||
|
if hasattr(param.modulation, "name")
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(param, "standard") and param.standard:
|
||||||
|
standard_name = (
|
||||||
|
param.standard.name
|
||||||
|
if hasattr(param.standard, "name")
|
||||||
|
else "-"
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(obj, "geo_obj") and obj.geo_obj:
|
||||||
|
comment = obj.geo_obj.comment or "-"
|
||||||
|
is_average = "Да" if obj.geo_obj.is_average else "Нет" if obj.geo_obj.is_average is not None else "-"
|
||||||
|
|
||||||
|
source_type = "ТВ" if obj.lyngsat_source else "-"
|
||||||
|
|
||||||
|
has_sigma = False
|
||||||
|
sigma_info = "-"
|
||||||
|
if param:
|
||||||
|
sigma_count = param.sigma_parameter.count()
|
||||||
|
if sigma_count > 0:
|
||||||
|
has_sigma = True
|
||||||
|
first_sigma = param.sigma_parameter.first()
|
||||||
|
if first_sigma:
|
||||||
|
sigma_freq = f"{first_sigma.transfer_frequency:.3f}" if first_sigma.transfer_frequency else "-"
|
||||||
|
sigma_range = f"{first_sigma.freq_range:.3f}" if first_sigma.freq_range else "-"
|
||||||
|
sigma_pol = first_sigma.polarization.name if first_sigma.polarization else "-"
|
||||||
|
sigma_pol_short = sigma_pol[0] if sigma_pol and sigma_pol != "-" else "-"
|
||||||
|
sigma_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}"
|
||||||
|
|
||||||
|
processed_objects.append(
|
||||||
|
{
|
||||||
|
"id": obj.id,
|
||||||
|
"name": obj.name or "-",
|
||||||
|
"satellite_name": satellite_name,
|
||||||
|
"frequency": frequency,
|
||||||
|
"freq_range": freq_range,
|
||||||
|
"polarization": polarization_name,
|
||||||
|
"bod_velocity": bod_velocity,
|
||||||
|
"modulation": modulation_name,
|
||||||
|
"snr": snr,
|
||||||
|
"geo_timestamp": geo_timestamp,
|
||||||
|
"geo_location": geo_location,
|
||||||
|
"geo_coords": geo_coords,
|
||||||
|
"kupsat_coords": kupsat_coords,
|
||||||
|
"valid_coords": valid_coords,
|
||||||
|
"distance_geo_kup": distance_geo_kup,
|
||||||
|
"distance_geo_valid": distance_geo_valid,
|
||||||
|
"distance_kup_valid": distance_kup_valid,
|
||||||
|
"updated_by": obj.updated_by if obj.updated_by else "-",
|
||||||
|
"comment": comment,
|
||||||
|
"is_average": is_average,
|
||||||
|
"source_type": source_type,
|
||||||
|
"standard": standard_name,
|
||||||
|
"has_sigma": has_sigma,
|
||||||
|
"sigma_info": sigma_info,
|
||||||
|
"obj": obj,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
modulations = Modulation.objects.all()
|
||||||
|
polarizations = Polarization.objects.all()
|
||||||
|
|
||||||
|
# Get the new filter values
|
||||||
|
has_source_type = request.GET.get("has_source_type")
|
||||||
|
has_sigma = request.GET.get("has_sigma")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"satellites": satellites,
|
||||||
|
"selected_satellite_id": selected_sat_id,
|
||||||
|
"page_obj": page_obj,
|
||||||
|
"processed_objects": processed_objects,
|
||||||
|
"items_per_page": items_per_page,
|
||||||
|
"available_items_per_page": [50, 100, 500, 1000],
|
||||||
|
"freq_min": freq_min,
|
||||||
|
"freq_max": freq_max,
|
||||||
|
"range_min": range_min,
|
||||||
|
"range_max": range_max,
|
||||||
|
"snr_min": snr_min,
|
||||||
|
"snr_max": snr_max,
|
||||||
|
"bod_min": bod_min,
|
||||||
|
"bod_max": bod_max,
|
||||||
|
"search_query": search_query,
|
||||||
|
"selected_modulations": [
|
||||||
|
int(x) for x in selected_modulations if x.isdigit()
|
||||||
|
],
|
||||||
|
"selected_polarizations": [
|
||||||
|
int(x) for x in selected_polarizations if x.isdigit()
|
||||||
|
],
|
||||||
|
"selected_satellites": [int(x) for x in selected_satellites if x.isdigit()],
|
||||||
|
"has_kupsat": has_kupsat,
|
||||||
|
"has_valid": has_valid,
|
||||||
|
"date_from": date_from,
|
||||||
|
"date_to": date_to,
|
||||||
|
"has_source_type": has_source_type,
|
||||||
|
"has_sigma": has_sigma,
|
||||||
|
"modulations": modulations,
|
||||||
|
"polarizations": polarizations,
|
||||||
|
"full_width_page": True,
|
||||||
|
"sort": sort_param,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "mainapp/objitem_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemFormView(
|
||||||
|
RoleRequiredMixin, CoordinateProcessingMixin, FormMessageMixin, UpdateView
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Base class for creating and editing ObjItem.
|
||||||
|
|
||||||
|
Contains common logic for form processing, coordinates, and parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = ObjItem
|
||||||
|
form_class = ObjItemForm
|
||||||
|
template_name = "mainapp/objitem_form.html"
|
||||||
|
success_url = reverse_lazy("mainapp:home")
|
||||||
|
required_roles = ["admin", "moderator"]
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
"""Returns URL with saved filter parameters."""
|
||||||
|
if self.request.GET:
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
query_string = urlencode(self.request.GET)
|
||||||
|
return reverse_lazy("mainapp:objitem_list") + '?' + query_string
|
||||||
|
return reverse_lazy("mainapp:objitem_list")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["LEAFLET_CONFIG"] = {
|
||||||
|
"DEFAULT_CENTER": (55.75, 37.62),
|
||||||
|
"DEFAULT_ZOOM": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save return parameters for "Back" button
|
||||||
|
context["return_params"] = self.request.GET.get('return_params', '')
|
||||||
|
|
||||||
|
# Work with single parameter form instead of formset
|
||||||
|
if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
|
||||||
|
context["parameter_form"] = ParameterForm(
|
||||||
|
instance=self.object.parameter_obj, prefix="parameter"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
context["parameter_form"] = ParameterForm(prefix="parameter")
|
||||||
|
|
||||||
|
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
|
||||||
|
context["geo_form"] = GeoForm(
|
||||||
|
instance=self.object.geo_obj, prefix="geo"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
context["geo_form"] = GeoForm(prefix="geo")
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# Get parameter form
|
||||||
|
if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
|
||||||
|
parameter_form = ParameterForm(
|
||||||
|
self.request.POST,
|
||||||
|
instance=self.object.parameter_obj,
|
||||||
|
prefix="parameter"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
parameter_form = ParameterForm(self.request.POST, prefix="parameter")
|
||||||
|
|
||||||
|
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
|
||||||
|
geo_form = GeoForm(self.request.POST, instance=self.object.geo_obj, prefix="geo")
|
||||||
|
else:
|
||||||
|
geo_form = GeoForm(self.request.POST, prefix="geo")
|
||||||
|
|
||||||
|
# Save main object
|
||||||
|
self.object = form.save(commit=False)
|
||||||
|
self.set_user_fields()
|
||||||
|
self.object.save()
|
||||||
|
|
||||||
|
# Save related parameter
|
||||||
|
if parameter_form.is_valid():
|
||||||
|
self.save_parameter(parameter_form)
|
||||||
|
else:
|
||||||
|
context = self.get_context_data()
|
||||||
|
context.update({
|
||||||
|
'form': form,
|
||||||
|
'parameter_form': parameter_form,
|
||||||
|
'geo_form': geo_form,
|
||||||
|
})
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
# Save geo data
|
||||||
|
if geo_form.is_valid():
|
||||||
|
self.save_geo_data(geo_form)
|
||||||
|
else:
|
||||||
|
context = self.get_context_data()
|
||||||
|
context.update({
|
||||||
|
'form': form,
|
||||||
|
'parameter_form': parameter_form,
|
||||||
|
'geo_form': geo_form,
|
||||||
|
})
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def set_user_fields(self):
|
||||||
|
"""Sets user fields for the object."""
|
||||||
|
raise NotImplementedError("Subclasses must implement set_user_fields()")
|
||||||
|
|
||||||
|
def save_parameter(self, parameter_form):
|
||||||
|
"""Saves object parameter through OneToOne relationship."""
|
||||||
|
if parameter_form.is_valid():
|
||||||
|
instance = parameter_form.save(commit=False)
|
||||||
|
instance.objitem = self.object
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
def save_geo_data(self, geo_form):
|
||||||
|
"""Saves object geo data."""
|
||||||
|
geo_instance = self.get_or_create_geo_instance()
|
||||||
|
|
||||||
|
# Update fields from geo_form
|
||||||
|
if geo_form.is_valid():
|
||||||
|
geo_instance.location = geo_form.cleaned_data["location"]
|
||||||
|
geo_instance.comment = geo_form.cleaned_data["comment"]
|
||||||
|
geo_instance.is_average = geo_form.cleaned_data["is_average"]
|
||||||
|
|
||||||
|
# Process date/time
|
||||||
|
self.process_timestamp(geo_instance)
|
||||||
|
|
||||||
|
geo_instance.save()
|
||||||
|
|
||||||
|
def get_or_create_geo_instance(self):
|
||||||
|
"""Gets or creates Geo instance."""
|
||||||
|
if hasattr(self.object, "geo_obj") and self.object.geo_obj:
|
||||||
|
return self.object.geo_obj
|
||||||
|
return Geo(objitem=self.object)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemUpdateView(ObjItemFormView):
|
||||||
|
"""View for editing ObjItem."""
|
||||||
|
|
||||||
|
success_message = "Объект успешно сохранён!"
|
||||||
|
|
||||||
|
def set_user_fields(self):
|
||||||
|
self.object.updated_by = self.request.user.customuser
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemCreateView(ObjItemFormView, CreateView):
|
||||||
|
"""View for creating ObjItem."""
|
||||||
|
|
||||||
|
success_message = "Объект успешно создан!"
|
||||||
|
|
||||||
|
def set_user_fields(self):
|
||||||
|
self.object.created_by = self.request.user.customuser
|
||||||
|
self.object.updated_by = self.request.user.customuser
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemDeleteView(RoleRequiredMixin, FormMessageMixin, DeleteView):
|
||||||
|
"""View for deleting ObjItem."""
|
||||||
|
|
||||||
|
model = ObjItem
|
||||||
|
template_name = "mainapp/objitem_confirm_delete.html"
|
||||||
|
success_url = reverse_lazy("mainapp:objitem_list")
|
||||||
|
success_message = "Объект успешно удалён!"
|
||||||
|
required_roles = ["admin", "moderator"]
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
"""Returns URL with saved filter parameters."""
|
||||||
|
if self.request.GET:
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
query_string = urlencode(self.request.GET)
|
||||||
|
return reverse_lazy("mainapp:objitem_list") + '?' + query_string
|
||||||
|
return reverse_lazy("mainapp:objitem_list")
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItemDetailView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
View for displaying ObjItem details in read-only mode.
|
||||||
|
|
||||||
|
Available to all authenticated users, displays data in read-only mode.
|
||||||
|
"""
|
||||||
|
def get(self, request, pk):
|
||||||
|
obj = ObjItem.objects.filter(pk=pk).select_related(
|
||||||
|
'geo_obj',
|
||||||
|
'updated_by__user',
|
||||||
|
'created_by__user',
|
||||||
|
'parameter_obj',
|
||||||
|
'parameter_obj__id_satellite',
|
||||||
|
'parameter_obj__polarization',
|
||||||
|
'parameter_obj__modulation',
|
||||||
|
'parameter_obj__standard',
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
from django.http import Http404
|
||||||
|
raise Http404("Объект не найден")
|
||||||
|
|
||||||
|
# Save return parameters for "Back" button
|
||||||
|
return_params = request.GET.get('return_params', '')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'object': obj,
|
||||||
|
'return_params': return_params
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "mainapp/objitem_detail.html", context)
|
||||||
190
dbapp/mainapp/views/source.py
Normal file
190
dbapp/mainapp/views/source.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"""
|
||||||
|
Source related views.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from ..models import Source
|
||||||
|
from ..utils import parse_pagination_params
|
||||||
|
|
||||||
|
|
||||||
|
class SourceListView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
View for displaying a list of sources (Source).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Get pagination parameters
|
||||||
|
page_number, items_per_page = parse_pagination_params(request)
|
||||||
|
|
||||||
|
# Get sorting parameters
|
||||||
|
sort_param = request.GET.get("sort", "-created_at")
|
||||||
|
|
||||||
|
# Get filter parameters
|
||||||
|
search_query = request.GET.get("search", "").strip()
|
||||||
|
has_coords_average = request.GET.get("has_coords_average")
|
||||||
|
has_coords_kupsat = request.GET.get("has_coords_kupsat")
|
||||||
|
has_coords_valid = request.GET.get("has_coords_valid")
|
||||||
|
has_coords_reference = request.GET.get("has_coords_reference")
|
||||||
|
objitem_count_min = request.GET.get("objitem_count_min", "").strip()
|
||||||
|
objitem_count_max = request.GET.get("objitem_count_max", "").strip()
|
||||||
|
date_from = request.GET.get("date_from", "").strip()
|
||||||
|
date_to = request.GET.get("date_to", "").strip()
|
||||||
|
|
||||||
|
# Get all Source objects with query optimization
|
||||||
|
sources = Source.objects.select_related(
|
||||||
|
'created_by__user',
|
||||||
|
'updated_by__user'
|
||||||
|
).prefetch_related(
|
||||||
|
'source_objitems',
|
||||||
|
'source_objitems__parameter_obj',
|
||||||
|
'source_objitems__geo_obj'
|
||||||
|
).annotate(
|
||||||
|
objitem_count=Count('source_objitems')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply filters
|
||||||
|
# Filter by coords_average presence
|
||||||
|
if has_coords_average == "1":
|
||||||
|
sources = sources.filter(coords_average__isnull=False)
|
||||||
|
elif has_coords_average == "0":
|
||||||
|
sources = sources.filter(coords_average__isnull=True)
|
||||||
|
|
||||||
|
# Filter by coords_kupsat presence
|
||||||
|
if has_coords_kupsat == "1":
|
||||||
|
sources = sources.filter(coords_kupsat__isnull=False)
|
||||||
|
elif has_coords_kupsat == "0":
|
||||||
|
sources = sources.filter(coords_kupsat__isnull=True)
|
||||||
|
|
||||||
|
# Filter by coords_valid presence
|
||||||
|
if has_coords_valid == "1":
|
||||||
|
sources = sources.filter(coords_valid__isnull=False)
|
||||||
|
elif has_coords_valid == "0":
|
||||||
|
sources = sources.filter(coords_valid__isnull=True)
|
||||||
|
|
||||||
|
# Filter by coords_reference presence
|
||||||
|
if has_coords_reference == "1":
|
||||||
|
sources = sources.filter(coords_reference__isnull=False)
|
||||||
|
elif has_coords_reference == "0":
|
||||||
|
sources = sources.filter(coords_reference__isnull=True)
|
||||||
|
|
||||||
|
# Filter by ObjItem count
|
||||||
|
if objitem_count_min:
|
||||||
|
try:
|
||||||
|
min_count = int(objitem_count_min)
|
||||||
|
sources = sources.filter(objitem_count__gte=min_count)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if objitem_count_max:
|
||||||
|
try:
|
||||||
|
max_count = int(objitem_count_max)
|
||||||
|
sources = sources.filter(objitem_count__lte=max_count)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter by creation date range
|
||||||
|
if date_from:
|
||||||
|
try:
|
||||||
|
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
|
||||||
|
sources = sources.filter(created_at__gte=date_from_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if date_to:
|
||||||
|
try:
|
||||||
|
from datetime import timedelta
|
||||||
|
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d")
|
||||||
|
# Add one day to include entire end date
|
||||||
|
date_to_obj = date_to_obj + timedelta(days=1)
|
||||||
|
sources = sources.filter(created_at__lt=date_to_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Search by ID
|
||||||
|
if search_query:
|
||||||
|
try:
|
||||||
|
search_id = int(search_query)
|
||||||
|
sources = sources.filter(id=search_id)
|
||||||
|
except ValueError:
|
||||||
|
# If not a number, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Apply sorting
|
||||||
|
valid_sort_fields = {
|
||||||
|
"id": "id",
|
||||||
|
"-id": "-id",
|
||||||
|
"created_at": "created_at",
|
||||||
|
"-created_at": "-created_at",
|
||||||
|
"updated_at": "updated_at",
|
||||||
|
"-updated_at": "-updated_at",
|
||||||
|
"objitem_count": "objitem_count",
|
||||||
|
"-objitem_count": "-objitem_count",
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort_param in valid_sort_fields:
|
||||||
|
sources = sources.order_by(valid_sort_fields[sort_param])
|
||||||
|
|
||||||
|
# Create paginator
|
||||||
|
paginator = Paginator(sources, items_per_page)
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
# Prepare data for display
|
||||||
|
processed_sources = []
|
||||||
|
for source in page_obj:
|
||||||
|
# Format coordinates
|
||||||
|
def format_coords(point):
|
||||||
|
if point:
|
||||||
|
longitude = point.coords[0]
|
||||||
|
latitude = point.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}"
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
coords_average_str = format_coords(source.coords_average)
|
||||||
|
coords_kupsat_str = format_coords(source.coords_kupsat)
|
||||||
|
coords_valid_str = format_coords(source.coords_valid)
|
||||||
|
coords_reference_str = format_coords(source.coords_reference)
|
||||||
|
|
||||||
|
# Get count of related ObjItems
|
||||||
|
objitem_count = source.objitem_count
|
||||||
|
|
||||||
|
processed_sources.append({
|
||||||
|
'id': source.id,
|
||||||
|
'coords_average': coords_average_str,
|
||||||
|
'coords_kupsat': coords_kupsat_str,
|
||||||
|
'coords_valid': coords_valid_str,
|
||||||
|
'coords_reference': coords_reference_str,
|
||||||
|
'objitem_count': objitem_count,
|
||||||
|
'created_at': source.created_at,
|
||||||
|
'updated_at': source.updated_at,
|
||||||
|
'created_by': source.created_by,
|
||||||
|
'updated_by': source.updated_by,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Prepare context for template
|
||||||
|
context = {
|
||||||
|
'page_obj': page_obj,
|
||||||
|
'processed_sources': processed_sources,
|
||||||
|
'items_per_page': items_per_page,
|
||||||
|
'available_items_per_page': [50, 100, 500, 1000],
|
||||||
|
'sort': sort_param,
|
||||||
|
'search_query': search_query,
|
||||||
|
'has_coords_average': has_coords_average,
|
||||||
|
'has_coords_kupsat': has_coords_kupsat,
|
||||||
|
'has_coords_valid': has_coords_valid,
|
||||||
|
'has_coords_reference': has_coords_reference,
|
||||||
|
'objitem_count_min': objitem_count_min,
|
||||||
|
'objitem_count_max': objitem_count_max,
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'full_width_page': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "mainapp/source_list.html", context)
|
||||||
@@ -61,15 +61,6 @@ class AddSatellitesView(LoginRequiredMixin, View):
|
|||||||
return redirect("mainapp:home")
|
return redirect("mainapp:home")
|
||||||
|
|
||||||
|
|
||||||
# class AddTranspondersView(View):
|
|
||||||
# def get(self, request):
|
|
||||||
# try:
|
|
||||||
# parse_transponders_from_json(BASE_DIR / "transponders.json")
|
|
||||||
# except FileNotFoundError:
|
|
||||||
# print("Файл не найден")
|
|
||||||
# return redirect('home')
|
|
||||||
|
|
||||||
|
|
||||||
class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
|
class AddTranspondersView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
template_name = "mainapp/transponders_upload.html"
|
template_name = "mainapp/transponders_upload.html"
|
||||||
form_class = UploadFileForm
|
form_class = UploadFileForm
|
||||||
@@ -1414,9 +1405,6 @@ class ObjItemFormView(
|
|||||||
geo_instance.comment = geo_form.cleaned_data["comment"]
|
geo_instance.comment = geo_form.cleaned_data["comment"]
|
||||||
geo_instance.is_average = geo_form.cleaned_data["is_average"]
|
geo_instance.is_average = geo_form.cleaned_data["is_average"]
|
||||||
|
|
||||||
# Обрабатываем координаты
|
|
||||||
self.process_coordinates(geo_instance)
|
|
||||||
|
|
||||||
# Обрабатываем дату/время
|
# Обрабатываем дату/время
|
||||||
self.process_timestamp(geo_instance)
|
self.process_timestamp(geo_instance)
|
||||||
|
|
||||||
Reference in New Issue
Block a user