Виджет с усреднёнными точками на карте
This commit is contained in:
@@ -31,12 +31,19 @@ from .lyngsat import (
|
||||
LyngsatTaskStatusView,
|
||||
ClearLyngsatCacheView,
|
||||
)
|
||||
from .source import SourceListView, SourceUpdateView, SourceDeleteView
|
||||
from .source import SourceListView, SourceUpdateView, SourceDeleteView, DeleteSelectedSourcesView
|
||||
from .transponder import (
|
||||
TransponderListView,
|
||||
TransponderCreateView,
|
||||
TransponderUpdateView,
|
||||
DeleteSelectedTranspondersView,
|
||||
)
|
||||
from .map import (
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
ShowSourceWithPointsMapView,
|
||||
ShowSourceAveragingStepsMapView,
|
||||
ClusterTestView,
|
||||
)
|
||||
|
||||
@@ -75,10 +82,17 @@ __all__ = [
|
||||
'SourceListView',
|
||||
'SourceUpdateView',
|
||||
'SourceDeleteView',
|
||||
'DeleteSelectedSourcesView',
|
||||
# Transponder
|
||||
'TransponderListView',
|
||||
'TransponderCreateView',
|
||||
'TransponderUpdateView',
|
||||
'DeleteSelectedTranspondersView',
|
||||
# Map
|
||||
'ShowMapView',
|
||||
'ShowSelectedObjectsMapView',
|
||||
'ShowSourcesMapView',
|
||||
'ShowSourceWithPointsMapView',
|
||||
'ShowSourceAveragingStepsMapView',
|
||||
'ClusterTestView',
|
||||
]
|
||||
|
||||
@@ -255,6 +255,158 @@ class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
|
||||
return render(request, "mainapp/source_with_points_map.html", context)
|
||||
|
||||
|
||||
class ShowSourceAveragingStepsMapView(LoginRequiredMixin, View):
|
||||
"""View for displaying source averaging steps visualization."""
|
||||
|
||||
def get(self, request, source_id):
|
||||
from ..models import Source
|
||||
from ..utils import calculate_mean_coords, RANGE_DISTANCE
|
||||
|
||||
try:
|
||||
source = Source.objects.prefetch_related(
|
||||
"source_objitems",
|
||||
"source_objitems__parameter_obj",
|
||||
"source_objitems__geo_obj",
|
||||
).get(id=source_id)
|
||||
except Source.DoesNotExist:
|
||||
return redirect("mainapp:home")
|
||||
|
||||
# Получаем все ObjItem, отсортированные по ID (порядок добавления)
|
||||
objitems = source.source_objitems.select_related(
|
||||
"parameter_obj", "geo_obj"
|
||||
).order_by("id")
|
||||
|
||||
# Собираем координаты всех точек
|
||||
original_points = []
|
||||
for obj in objitems:
|
||||
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
|
||||
|
||||
original_points.append(
|
||||
{
|
||||
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
||||
"name": obj.name,
|
||||
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
|
||||
"objitem_id": obj.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Воспроизводим алгоритм усреднения
|
||||
averaging_steps = []
|
||||
|
||||
if original_points:
|
||||
# Первая точка становится начальным средним
|
||||
current_avg = original_points[0]["point"]
|
||||
|
||||
# Обрабатываем остальные точки
|
||||
for i, point_data in enumerate(original_points[1:], start=1):
|
||||
current_coord = point_data["point"]
|
||||
|
||||
# Вычисляем новое среднее и расстояние
|
||||
new_avg, distance = calculate_mean_coords(current_avg, current_coord)
|
||||
|
||||
# Сохраняем шаг усреднения
|
||||
averaging_steps.append(
|
||||
{
|
||||
"point": new_avg,
|
||||
"step": i,
|
||||
"distance": round(distance, 2),
|
||||
"within_range": distance <= RANGE_DISTANCE,
|
||||
}
|
||||
)
|
||||
|
||||
# Обновляем текущее среднее
|
||||
current_avg = new_avg
|
||||
|
||||
# Формируем группы для отображения на карте
|
||||
groups = []
|
||||
|
||||
# 1. Исходные точки ObjItem (красный)
|
||||
if original_points:
|
||||
groups.append(
|
||||
{
|
||||
"name": "Исходные точки ГЛ",
|
||||
"points": original_points,
|
||||
"color": "red",
|
||||
}
|
||||
)
|
||||
|
||||
# 2. Промежуточные точки усреднения (оранжевый)
|
||||
if averaging_steps:
|
||||
intermediate_points = [
|
||||
{
|
||||
"point": step["point"],
|
||||
"step": f"Шаг {step['step']}",
|
||||
"distance": f"{step['distance']} км",
|
||||
}
|
||||
for step in averaging_steps[:-1] # Все кроме последней
|
||||
]
|
||||
if intermediate_points:
|
||||
groups.append(
|
||||
{
|
||||
"name": "Промежуточные шаги усреднения",
|
||||
"points": intermediate_points,
|
||||
"color": "orange",
|
||||
}
|
||||
)
|
||||
|
||||
# 3. Финальная усредненная точка (синий)
|
||||
if averaging_steps:
|
||||
final_step = averaging_steps[-1]
|
||||
groups.append(
|
||||
{
|
||||
"name": "Финальная усредненная координата",
|
||||
"points": [
|
||||
{
|
||||
"point": final_step["point"],
|
||||
"step": f"Шаг {final_step['step']} (финальный)",
|
||||
"distance": f"{final_step['distance']} км",
|
||||
}
|
||||
],
|
||||
"color": "blue",
|
||||
}
|
||||
)
|
||||
|
||||
# 4. Координаты источника для сравнения (если есть)
|
||||
source_coord_types = [
|
||||
("coords_average", "Сохраненные усредненные координаты", "green"),
|
||||
("coords_kupsat", "Координаты Кубсата", "purple"),
|
||||
("coords_valid", "Координаты оперативников", "cyan"),
|
||||
("coords_reference", "Координаты справочные", "violet"),
|
||||
]
|
||||
|
||||
for coord_field, label, color in source_coord_types:
|
||||
coords = getattr(source, coord_field)
|
||||
if coords:
|
||||
groups.append(
|
||||
{
|
||||
"name": label,
|
||||
"points": [
|
||||
{
|
||||
"point": (coords.x, coords.y),
|
||||
"source_id": f"Источник #{source.id}",
|
||||
}
|
||||
],
|
||||
"color": color,
|
||||
}
|
||||
)
|
||||
|
||||
context = {
|
||||
"groups": groups,
|
||||
"source_id": source_id,
|
||||
"total_points": len(original_points),
|
||||
"total_steps": len(averaging_steps),
|
||||
}
|
||||
return render(request, "mainapp/source_averaging_map.html", context)
|
||||
|
||||
|
||||
class ClusterTestView(LoginRequiredMixin, View):
|
||||
"""Test view for clustering functionality."""
|
||||
|
||||
|
||||
@@ -55,6 +55,13 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
)
|
||||
|
||||
selected_sat_id = request.GET.get("satellite_id")
|
||||
|
||||
# If no satellite is selected and no filters are applied, select the first satellite
|
||||
if not selected_sat_id and not request.GET.getlist("satellite_id"):
|
||||
first_satellite = satellites.first()
|
||||
if first_satellite:
|
||||
selected_sat_id = str(first_satellite.id)
|
||||
|
||||
page_number, items_per_page = parse_pagination_params(request)
|
||||
sort_param = request.GET.get("sort", "")
|
||||
|
||||
@@ -450,12 +457,14 @@ class ObjItemListView(LoginRequiredMixin, View):
|
||||
"bod_max": bod_max,
|
||||
"search_query": search_query,
|
||||
"selected_modulations": [
|
||||
int(x) for x in selected_modulations if x.isdigit()
|
||||
int(x) if isinstance(x, str) else x for x in selected_modulations if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
"selected_polarizations": [
|
||||
int(x) for x in selected_polarizations if x.isdigit()
|
||||
int(x) if isinstance(x, str) else x for x in selected_polarizations if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
"selected_satellites": [
|
||||
int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and 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,
|
||||
|
||||
@@ -6,13 +6,14 @@ from datetime import datetime
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from ..forms import SourceForm
|
||||
from ..models import Source
|
||||
from ..models import Source, Satellite
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
|
||||
@@ -38,6 +39,15 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
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()
|
||||
selected_satellites = request.GET.getlist("satellite_id")
|
||||
|
||||
# Get all satellites for filter
|
||||
satellites = (
|
||||
Satellite.objects.filter(parameters__objitem__source__isnull=False)
|
||||
.distinct()
|
||||
.only("id", "name")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
# Get all Source objects with query optimization
|
||||
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
|
||||
@@ -45,6 +55,7 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
sources = Source.objects.prefetch_related(
|
||||
'source_objitems',
|
||||
'source_objitems__parameter_obj',
|
||||
'source_objitems__parameter_obj__id_satellite',
|
||||
'source_objitems__geo_obj'
|
||||
).annotate(
|
||||
objitem_count=Count('source_objitems')
|
||||
@@ -117,6 +128,12 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
# If not a number, ignore
|
||||
pass
|
||||
|
||||
# Filter by satellites
|
||||
if selected_satellites:
|
||||
sources = sources.filter(
|
||||
source_objitems__parameter_obj__id_satellite_id__in=selected_satellites
|
||||
).distinct()
|
||||
|
||||
# Apply sorting
|
||||
valid_sort_fields = {
|
||||
"id": "id",
|
||||
@@ -157,6 +174,15 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
# Get count of related ObjItems
|
||||
objitem_count = source.objitem_count
|
||||
|
||||
# Get satellites for this source
|
||||
satellite_names = set()
|
||||
for objitem in source.source_objitems.all():
|
||||
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
|
||||
if hasattr(objitem.parameter_obj, 'id_satellite') and objitem.parameter_obj.id_satellite:
|
||||
satellite_names.add(objitem.parameter_obj.id_satellite.name)
|
||||
|
||||
satellite_str = ", ".join(sorted(satellite_names)) if satellite_names else "-"
|
||||
|
||||
processed_sources.append({
|
||||
'id': source.id,
|
||||
'coords_average': coords_average_str,
|
||||
@@ -164,6 +190,7 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'coords_valid': coords_valid_str,
|
||||
'coords_reference': coords_reference_str,
|
||||
'objitem_count': objitem_count,
|
||||
'satellite': satellite_str,
|
||||
'created_at': source.created_at,
|
||||
'updated_at': source.updated_at,
|
||||
})
|
||||
@@ -184,6 +211,10 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
'objitem_count_max': objitem_count_max,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'satellites': satellites,
|
||||
'selected_satellites': [
|
||||
int(x) if isinstance(x, str) else x for x in selected_satellites if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
@@ -302,3 +333,89 @@ class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
if request.GET.urlencode():
|
||||
return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}")
|
||||
return redirect('mainapp:home')
|
||||
|
||||
|
||||
class DeleteSelectedSourcesView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
"""View for deleting multiple selected sources with confirmation."""
|
||||
|
||||
def get(self, request):
|
||||
"""Show confirmation page with details about sources to be deleted."""
|
||||
ids = request.GET.get("ids", "")
|
||||
if not ids:
|
||||
messages.error(request, "Не выбраны источники для удаления")
|
||||
return redirect('mainapp:home')
|
||||
|
||||
try:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
sources = Source.objects.filter(id__in=id_list).prefetch_related(
|
||||
'source_objitems',
|
||||
'source_objitems__parameter_obj',
|
||||
'source_objitems__parameter_obj__id_satellite',
|
||||
'source_objitems__geo_obj'
|
||||
).annotate(
|
||||
objitem_count=Count('source_objitems')
|
||||
)
|
||||
|
||||
# Prepare detailed information about sources
|
||||
sources_info = []
|
||||
total_objitems = 0
|
||||
|
||||
for source in sources:
|
||||
# Get satellites for this source
|
||||
satellite_names = set()
|
||||
for objitem in source.source_objitems.all():
|
||||
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
|
||||
if hasattr(objitem.parameter_obj, 'id_satellite') and objitem.parameter_obj.id_satellite:
|
||||
satellite_names.add(objitem.parameter_obj.id_satellite.name)
|
||||
|
||||
objitem_count = source.objitem_count
|
||||
total_objitems += objitem_count
|
||||
|
||||
sources_info.append({
|
||||
'id': source.id,
|
||||
'objitem_count': objitem_count,
|
||||
'satellites': ", ".join(sorted(satellite_names)) if satellite_names else "-",
|
||||
})
|
||||
|
||||
context = {
|
||||
'sources_info': sources_info,
|
||||
'total_sources': len(sources_info),
|
||||
'total_objitems': total_objitems,
|
||||
'ids': ids,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_bulk_delete_confirm.html', context)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при подготовке удаления: {str(e)}')
|
||||
return redirect('mainapp:home')
|
||||
|
||||
def post(self, request):
|
||||
"""Actually delete the selected sources."""
|
||||
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()]
|
||||
|
||||
# Get count before deletion
|
||||
sources = Source.objects.filter(id__in=id_list)
|
||||
deleted_sources_count = sources.count()
|
||||
|
||||
# Delete sources (cascade will delete related objitems)
|
||||
sources.delete()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Успешно удалено источников: {deleted_sources_count}'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"message": f"Успешно удалено источников: {deleted_sources_count}",
|
||||
"deleted_count": deleted_sources_count,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": f"Ошибка при удалении: {str(e)}"}, status=500)
|
||||
|
||||
374
dbapp/mainapp/views/transponder.py
Normal file
374
dbapp/mainapp/views/transponder.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
Transponder CRUD operations and related views.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count, Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import CreateView, UpdateView
|
||||
|
||||
from mapsapp.models import Transponders
|
||||
from ..forms import TransponderForm
|
||||
from ..mixins import RoleRequiredMixin, FormMessageMixin
|
||||
from ..models import Satellite, Polarization
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
|
||||
class TransponderListView(LoginRequiredMixin, View):
|
||||
"""View for displaying a list of transponders with filtering and pagination."""
|
||||
|
||||
def get(self, request):
|
||||
# Get pagination parameters
|
||||
page_number, items_per_page = parse_pagination_params(request)
|
||||
|
||||
# Get sorting parameters (default to satellite and downlink)
|
||||
sort_param = request.GET.get("sort", "sat_id__name")
|
||||
|
||||
# Get filter parameters
|
||||
search_query = request.GET.get("search", "").strip()
|
||||
selected_satellites = request.GET.getlist("satellite_id")
|
||||
selected_polarizations = request.GET.getlist("polarization")
|
||||
downlink_min = request.GET.get("downlink_min", "").strip()
|
||||
downlink_max = request.GET.get("downlink_max", "").strip()
|
||||
uplink_min = request.GET.get("uplink_min", "").strip()
|
||||
uplink_max = request.GET.get("uplink_max", "").strip()
|
||||
freq_range_min = request.GET.get("freq_range_min", "").strip()
|
||||
freq_range_max = request.GET.get("freq_range_max", "").strip()
|
||||
snr_min = request.GET.get("snr_min", "").strip()
|
||||
snr_max = request.GET.get("snr_max", "").strip()
|
||||
date_from = request.GET.get("date_from", "").strip()
|
||||
date_to = request.GET.get("date_to", "").strip()
|
||||
|
||||
# Get all satellites and polarizations for filters
|
||||
satellites = Satellite.objects.filter(
|
||||
tran_satellite__isnull=False
|
||||
).distinct().only("id", "name").order_by("name")
|
||||
|
||||
polarizations = Polarization.objects.all().order_by("name")
|
||||
|
||||
# Get all transponders with query optimization
|
||||
transponders = Transponders.objects.select_related(
|
||||
'sat_id',
|
||||
'polarization',
|
||||
'created_by__user',
|
||||
'updated_by__user'
|
||||
).annotate(
|
||||
objitem_count=Count('transponder_objitems')
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
# Filter by satellites
|
||||
if selected_satellites:
|
||||
transponders = transponders.filter(sat_id_id__in=selected_satellites)
|
||||
|
||||
# Filter by polarizations
|
||||
if selected_polarizations:
|
||||
transponders = transponders.filter(polarization_id__in=selected_polarizations)
|
||||
|
||||
# Filter by downlink frequency
|
||||
if downlink_min:
|
||||
try:
|
||||
min_val = float(downlink_min)
|
||||
transponders = transponders.filter(downlink__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if downlink_max:
|
||||
try:
|
||||
max_val = float(downlink_max)
|
||||
transponders = transponders.filter(downlink__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by uplink frequency
|
||||
if uplink_min:
|
||||
try:
|
||||
min_val = float(uplink_min)
|
||||
transponders = transponders.filter(uplink__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if uplink_max:
|
||||
try:
|
||||
max_val = float(uplink_max)
|
||||
transponders = transponders.filter(uplink__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by frequency range
|
||||
if freq_range_min:
|
||||
try:
|
||||
min_val = float(freq_range_min)
|
||||
transponders = transponders.filter(frequency_range__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if freq_range_max:
|
||||
try:
|
||||
max_val = float(freq_range_max)
|
||||
transponders = transponders.filter(frequency_range__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by SNR
|
||||
if snr_min:
|
||||
try:
|
||||
min_val = float(snr_min)
|
||||
transponders = transponders.filter(snr__gte=min_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if snr_max:
|
||||
try:
|
||||
max_val = float(snr_max)
|
||||
transponders = transponders.filter(snr__lte=max_val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Filter by creation date range
|
||||
if date_from:
|
||||
try:
|
||||
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
|
||||
transponders = transponders.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)
|
||||
transponders = transponders.filter(created_at__lt=date_to_obj)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Search by name or zone name
|
||||
if search_query:
|
||||
transponders = transponders.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(zone_name__icontains=search_query) |
|
||||
Q(sat_id__name__icontains=search_query)
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
valid_sort_fields = {
|
||||
"sat_id__name": "sat_id__name",
|
||||
"-sat_id__name": "-sat_id__name",
|
||||
"name": "name",
|
||||
"-name": "-name",
|
||||
"downlink": "downlink",
|
||||
"-downlink": "-downlink",
|
||||
"uplink": "uplink",
|
||||
"-uplink": "-uplink",
|
||||
"frequency_range": "frequency_range",
|
||||
"-frequency_range": "-frequency_range",
|
||||
"zone_name": "zone_name",
|
||||
"-zone_name": "-zone_name",
|
||||
"polarization__name": "polarization__name",
|
||||
"-polarization__name": "-polarization__name",
|
||||
"snr": "snr",
|
||||
"-snr": "-snr",
|
||||
"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:
|
||||
transponders = transponders.order_by(valid_sort_fields[sort_param])
|
||||
|
||||
# Create paginator
|
||||
paginator = Paginator(transponders, items_per_page)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Prepare data for display
|
||||
processed_transponders = []
|
||||
for transponder in page_obj:
|
||||
processed_transponders.append({
|
||||
'id': transponder.id,
|
||||
'name': transponder.name or "-",
|
||||
'satellite': transponder.sat_id.name if transponder.sat_id else "-",
|
||||
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else "-",
|
||||
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else "-",
|
||||
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else "-",
|
||||
'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else "-",
|
||||
'zone_name': transponder.zone_name or "-",
|
||||
'polarization': transponder.polarization.name if transponder.polarization else "-",
|
||||
'snr': f"{transponder.snr:.1f}" if transponder.snr else "-",
|
||||
'objitem_count': transponder.objitem_count,
|
||||
'created_at': transponder.created_at,
|
||||
'updated_at': transponder.updated_at,
|
||||
'created_by': transponder.created_by if transponder.created_by else "-",
|
||||
'updated_by': transponder.updated_by if transponder.updated_by else "-",
|
||||
})
|
||||
|
||||
# Prepare context for template
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'processed_transponders': processed_transponders,
|
||||
'items_per_page': items_per_page,
|
||||
'available_items_per_page': [50, 100, 500, 1000],
|
||||
'sort': sort_param,
|
||||
'search_query': search_query,
|
||||
'satellites': satellites,
|
||||
'polarizations': polarizations,
|
||||
'selected_satellites': [
|
||||
int(x) if isinstance(x, str) else x for x in selected_satellites
|
||||
if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
'selected_polarizations': [
|
||||
int(x) if isinstance(x, str) else x for x in selected_polarizations
|
||||
if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||
],
|
||||
'downlink_min': downlink_min,
|
||||
'downlink_max': downlink_max,
|
||||
'uplink_min': uplink_min,
|
||||
'uplink_max': uplink_max,
|
||||
'freq_range_min': freq_range_min,
|
||||
'freq_range_max': freq_range_max,
|
||||
'snr_min': snr_min,
|
||||
'snr_max': snr_max,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, "mainapp/transponder_list.html", context)
|
||||
|
||||
|
||||
class TransponderCreateView(RoleRequiredMixin, FormMessageMixin, CreateView):
|
||||
"""View for creating a new transponder."""
|
||||
|
||||
model = Transponders
|
||||
form_class = TransponderForm
|
||||
template_name = "mainapp/transponder_form.html"
|
||||
success_url = reverse_lazy("mainapp:transponder_list")
|
||||
success_message = "Транспондер успешно создан!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['action'] = 'create'
|
||||
context['title'] = 'Создание транспондера'
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.created_by = self.request.user.customuser
|
||||
form.instance.updated_by = self.request.user.customuser
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class TransponderUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
|
||||
"""View for updating an existing transponder."""
|
||||
|
||||
model = Transponders
|
||||
form_class = TransponderForm
|
||||
template_name = "mainapp/transponder_form.html"
|
||||
success_url = reverse_lazy("mainapp:transponder_list")
|
||||
success_message = "Транспондер успешно обновлен!"
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['action'] = 'update'
|
||||
context['title'] = f'Редактирование транспондера #{self.object.id}'
|
||||
|
||||
# Get related objitems count
|
||||
context['objitem_count'] = self.object.transponder_objitems.count()
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.updated_by = self.request.user.customuser
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DeleteSelectedTranspondersView(RoleRequiredMixin, View):
|
||||
"""View for deleting multiple selected transponders with confirmation."""
|
||||
|
||||
required_roles = ["admin", "moderator"]
|
||||
|
||||
def get(self, request):
|
||||
"""Show confirmation page with details about transponders to be deleted."""
|
||||
ids = request.GET.get("ids", "")
|
||||
if not ids:
|
||||
messages.error(request, "Не выбраны транспондеры для удаления")
|
||||
return redirect('mainapp:transponder_list')
|
||||
|
||||
try:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
transponders = Transponders.objects.filter(id__in=id_list).select_related(
|
||||
'sat_id',
|
||||
'polarization'
|
||||
).annotate(
|
||||
objitem_count=Count('transponder_objitems')
|
||||
)
|
||||
|
||||
# Prepare detailed information about transponders
|
||||
transponders_info = []
|
||||
total_objitems = 0
|
||||
|
||||
for transponder in transponders:
|
||||
objitem_count = transponder.objitem_count
|
||||
total_objitems += objitem_count
|
||||
|
||||
transponders_info.append({
|
||||
'id': transponder.id,
|
||||
'name': transponder.name or "-",
|
||||
'satellite': transponder.sat_id.name if transponder.sat_id else "-",
|
||||
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else "-",
|
||||
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else "-",
|
||||
'objitem_count': objitem_count,
|
||||
})
|
||||
|
||||
context = {
|
||||
'transponders_info': transponders_info,
|
||||
'total_transponders': len(transponders_info),
|
||||
'total_objitems': total_objitems,
|
||||
'ids': ids,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/transponder_bulk_delete_confirm.html', context)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при подготовке удаления: {str(e)}')
|
||||
return redirect('mainapp:transponder_list')
|
||||
|
||||
def post(self, request):
|
||||
"""Actually delete the selected transponders."""
|
||||
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()]
|
||||
|
||||
# Get count before deletion
|
||||
transponders = Transponders.objects.filter(id__in=id_list)
|
||||
deleted_count = transponders.count()
|
||||
|
||||
# Delete transponders (cascade will handle related objitems)
|
||||
transponders.delete()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Успешно удалено транспондеров: {deleted_count}'
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"message": f"Успешно удалено транспондеров: {deleted_count}",
|
||||
"deleted_count": deleted_count,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": f"Ошибка при удалении: {str(e)}"}, status=500)
|
||||
Reference in New Issue
Block a user