Files
dbstorage/dbapp/mainapp/views/objitem.py

728 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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, Prefetch
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 (
format_coordinate,
format_coords_display,
format_frequency,
format_symbol_rate,
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):
import json
from datetime import datetime, timedelta
from django.contrib.gis.geos import Polygon
from ..models import Standard
page_number, items_per_page = parse_pagination_params(request)
sort_param = request.GET.get("sort", "-id")
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_standards = request.GET.getlist("standard")
selected_satellites = request.GET.getlist("satellite")
selected_mirrors = request.GET.getlist("mirror")
selected_complexes = request.GET.getlist("complex")
date_from = request.GET.get("date_from")
date_to = request.GET.get("date_to")
polygon_coords = request.GET.get("polygon")
# Create optimized prefetch for mirrors through geo_obj
mirrors_prefetch = Prefetch(
'geo_obj__mirrors',
queryset=Satellite.objects.only('id', 'name').order_by('id')
)
# Load all objects without satellite filter
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",
"transponder",
"transponder__sat_id",
"transponder__polarization",
).prefetch_related(
mirrors_prefetch,
)
# Apply frequency filters
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
# Apply range filters
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
# Apply SNR filters
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
# Apply symbol rate filters
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
# Apply modulation filter
if selected_modulations:
objects = objects.filter(
parameter_obj__modulation__id__in=selected_modulations
)
# Apply polarization filter
if selected_polarizations:
objects = objects.filter(
parameter_obj__polarization__id__in=selected_polarizations
)
# Apply standard filter
if selected_standards:
objects = objects.filter(
parameter_obj__standard__id__in=selected_standards
)
# Apply satellite filter
if selected_satellites:
objects = objects.filter(
parameter_obj__id_satellite__id__in=selected_satellites
)
# Apply mirrors filter
if selected_mirrors:
objects = objects.filter(
geo_obj__mirrors__id__in=selected_mirrors
).distinct()
# Apply complex filter (location_place)
if selected_complexes:
objects = objects.filter(
parameter_obj__id_satellite__location_place__in=selected_complexes
)
# Date filter for geo_obj timestamp
if date_from and date_from.strip():
try:
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:
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 is_automatic
is_automatic_filter = request.GET.get("is_automatic")
if is_automatic_filter == "1":
objects = objects.filter(is_automatic=True)
elif is_automatic_filter == "0":
objects = objects.filter(is_automatic=False)
# Apply polygon filter
if polygon_coords:
try:
coords = json.loads(polygon_coords)
if coords and len(coords) >= 3:
# Ensure polygon is closed
if coords[0] != coords[-1]:
coords.append(coords[0])
polygon = Polygon(coords, srid=4326)
objects = objects.filter(geo_obj__coords__within=polygon)
except (json.JSONDecodeError, ValueError, TypeError):
pass
# Apply search filter
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)
)
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"),
)
# Define valid sort fields with their database mappings
valid_sort_fields = {
"id": "id",
"-id": "-id",
"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",
"is_automatic": "is_automatic",
"-is_automatic": "-is_automatic",
}
# Apply sorting if valid, otherwise use default
if sort_param in valid_sort_fields:
objects = objects.order_by(valid_sort_fields[sort_param])
else:
# Default sort by id descending
objects = objects.order_by("-id")
paginator = Paginator(objects, items_per_page)
page_obj = paginator.get_page(page_number)
processed_objects = []
for obj in page_obj:
param = getattr(obj, 'parameter_obj', None)
geo_coords = "-"
geo_timestamp = "-"
geo_location = "-"
kupsat_coords = "-"
valid_coords = "-"
distance_geo_kup = "-"
distance_geo_valid = "-"
distance_kup_valid = "-"
mirrors_list = []
if hasattr(obj, "geo_obj") and obj.geo_obj:
geo_timestamp = obj.geo_obj.timestamp
geo_location = obj.geo_obj.location
# Get mirrors - use prefetched data
mirrors_list = [mirror.name for mirror in obj.geo_obj.mirrors.all()]
if obj.geo_obj.coords:
geo_coords = format_coords_display(obj.geo_obj.coords)
satellite_name = "-"
satellite_id = None
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 "-"
)
satellite_id = param.id_satellite.id
frequency = format_frequency(param.frequency)
freq_range = format_frequency(param.freq_range)
bod_velocity = format_symbol_rate(param.bod_velocity)
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 "-"
# Build mirrors display with clickable links
mirrors_display = "-"
if mirrors_list:
mirrors_links = []
for mirror in obj.geo_obj.mirrors.all():
mirrors_links.append(
f'<a href="#" class="text-decoration-underline" '
f'onclick="showSatelliteModal({mirror.id}); return false;">{mirror.name}</a>'
)
mirrors_display = ", ".join(mirrors_links) if mirrors_links else "-"
processed_objects.append(
{
"id": obj.id,
"name": obj.name or "-",
"satellite_name": satellite_name,
"satellite_id": satellite_id,
"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,
"mirrors": ", ".join(mirrors_list) if mirrors_list else "-",
"mirrors_display": mirrors_display,
"is_automatic": "Да" if obj.is_automatic else "Нет",
"obj": obj,
}
)
modulations = Modulation.objects.all()
polarizations = Polarization.objects.all()
standards = Standard.objects.all()
# Get satellites for filter (only those used in parameters)
satellites = (
Satellite.objects.filter(parameters__isnull=False)
.distinct()
.only("id", "name")
.order_by("name")
)
# Get mirrors for filter (only those used in geo objects)
mirrors = (
Satellite.objects.filter(geo_mirrors__isnull=False)
.distinct()
.only("id", "name")
.order_by("name")
)
# Get complexes for filter
complexes = [
("kr", "КР"),
("dv", "ДВ")
]
context = {
"page_obj": page_obj,
"processed_objects": processed_objects,
"items_per_page": items_per_page,
"available_items_per_page": [50, 100, 200, 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) 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) if isinstance(x, str) else x for x in selected_polarizations if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
],
"selected_standards": [
int(x) if isinstance(x, str) else x for x in selected_standards 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_mirrors": [
int(x) if isinstance(x, str) else x for x in selected_mirrors if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
],
"selected_complexes": selected_complexes,
"date_from": date_from,
"date_to": date_to,
"is_automatic": is_automatic_filter,
"modulations": modulations,
"polarizations": polarizations,
"standards": standards,
"satellites": satellites,
"mirrors": mirrors,
"complexes": complexes,
"polygon_coords": polygon_coords,
"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:source_list")
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 get_object(self, queryset=None):
"""Override to add select_related for transponder."""
obj = super().get_object(queryset)
if obj and hasattr(obj, 'transponder'):
# Prefetch transponder data
obj = ObjItem.objects.select_related(
'transponder',
'transponder__sat_id',
'transponder__polarization',
'transponder__created_by__user',
).get(pk=obj.pk)
return obj
def form_valid(self, form):
# Get parameter form
if self.object and hasattr(self.object, "parameter_obj") and self.object.parameter_obj:
parameter_form = ParameterForm(
self.request.POST,
instance=self.object.parameter_obj,
prefix="parameter"
)
else:
parameter_form = ParameterForm(self.request.POST, prefix="parameter")
if self.object and hasattr(self.object, "geo_obj") and self.object.geo_obj:
geo_form = GeoForm(self.request.POST, instance=self.object.geo_obj, prefix="geo")
else:
geo_form = GeoForm(self.request.POST, prefix="geo")
# Save main object
self.object = form.save(commit=False)
self.set_user_fields()
self.object.save()
# Save related parameter
if parameter_form.is_valid():
self.save_parameter(parameter_form)
else:
context = self.get_context_data()
context.update({
'form': form,
'parameter_form': parameter_form,
'geo_form': geo_form,
})
return self.render_to_response(context)
# Save geo data
if geo_form.is_valid():
self.save_geo_data(geo_form)
else:
context = self.get_context_data()
context.update({
'form': form,
'parameter_form': parameter_form,
'geo_form': geo_form,
})
return self.render_to_response(context)
return super().form_valid(form)
def set_user_fields(self):
"""Sets user fields for the object."""
raise NotImplementedError("Subclasses must implement set_user_fields()")
def save_parameter(self, parameter_form):
"""Saves object parameter through OneToOne relationship."""
if parameter_form.is_valid():
instance = parameter_form.save(commit=False)
instance.objitem = self.object
instance.save()
def save_geo_data(self, geo_form):
"""Saves object geo data."""
geo_instance = self.get_or_create_geo_instance()
# Update fields from geo_form
if geo_form.is_valid():
geo_instance.location = geo_form.cleaned_data["location"]
geo_instance.comment = geo_form.cleaned_data["comment"]
geo_instance.is_average = geo_form.cleaned_data["is_average"]
# Process date/time
self.process_timestamp(geo_instance)
geo_instance.save()
# Save ManyToMany relationship for mirrors
if geo_form.is_valid():
geo_instance.mirrors.set(geo_form.cleaned_data["mirrors"])
def get_or_create_geo_instance(self):
"""Gets or creates Geo instance."""
if hasattr(self.object, "geo_obj") and self.object.geo_obj:
return self.object.geo_obj
return Geo(objitem=self.object)
class ObjItemUpdateView(ObjItemFormView):
"""View for editing ObjItem."""
success_message = "Объект успешно сохранён!"
def set_user_fields(self):
self.object.updated_by = self.request.user.customuser
class ObjItemCreateView(ObjItemFormView, CreateView):
"""View for creating ObjItem."""
success_message = "Объект успешно создан!"
def get_object(self, queryset=None):
"""Return None for create view."""
return None
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',
'transponder',
'transponder__sat_id',
'transponder__polarization',
'transponder__created_by__user',
).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)