""" Satellite 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 ..forms import SatelliteForm from ..mixins import RoleRequiredMixin, FormMessageMixin from ..models import Satellite, Band from ..utils import parse_pagination_params class SatelliteListView(LoginRequiredMixin, View): """View for displaying a list of satellites 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 name) sort_param = request.GET.get("sort", "name") # Get filter parameters search_query = request.GET.get("search", "").strip() selected_bands = request.GET.getlist("band_id") norad_min = request.GET.get("norad_min", "").strip() norad_max = request.GET.get("norad_max", "").strip() undersat_point_min = request.GET.get("undersat_point_min", "").strip() undersat_point_max = request.GET.get("undersat_point_max", "").strip() launch_date_from = request.GET.get("launch_date_from", "").strip() launch_date_to = request.GET.get("launch_date_to", "").strip() date_from = request.GET.get("date_from", "").strip() date_to = request.GET.get("date_to", "").strip() # Get all bands for filters bands = Band.objects.all().order_by("name") # Get all satellites with query optimization satellites = Satellite.objects.prefetch_related( 'band', 'created_by__user', 'updated_by__user' ).annotate( transponder_count=Count('tran_satellite', distinct=True) ) # Apply filters # Filter by bands if selected_bands: satellites = satellites.filter(band__id__in=selected_bands).distinct() # Filter by NORAD ID if norad_min: try: min_val = int(norad_min) satellites = satellites.filter(norad__gte=min_val) except ValueError: pass if norad_max: try: max_val = int(norad_max) satellites = satellites.filter(norad__lte=max_val) except ValueError: pass # Filter by undersat point if undersat_point_min: try: min_val = float(undersat_point_min) satellites = satellites.filter(undersat_point__gte=min_val) except ValueError: pass if undersat_point_max: try: max_val = float(undersat_point_max) satellites = satellites.filter(undersat_point__lte=max_val) except ValueError: pass # Filter by launch date range if launch_date_from: try: date_from_obj = datetime.strptime(launch_date_from, "%Y-%m-%d").date() satellites = satellites.filter(launch_date__gte=date_from_obj) except (ValueError, TypeError): pass if launch_date_to: try: from datetime import timedelta date_to_obj = datetime.strptime(launch_date_to, "%Y-%m-%d").date() # Add one day to include entire end date date_to_obj = date_to_obj + timedelta(days=1) satellites = satellites.filter(launch_date__lt=date_to_obj) except (ValueError, TypeError): pass # Filter by creation date range if date_from: try: date_from_obj = datetime.strptime(date_from, "%Y-%m-%d") satellites = satellites.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) satellites = satellites.filter(created_at__lt=date_to_obj) except (ValueError, TypeError): pass # Search by name if search_query: satellites = satellites.filter( Q(name__icontains=search_query) | Q(comment__icontains=search_query) ) # Apply sorting valid_sort_fields = { "id": "id", "-id": "-id", "name": "name", "-name": "-name", "norad": "norad", "-norad": "-norad", "undersat_point": "undersat_point", "-undersat_point": "-undersat_point", "launch_date": "launch_date", "-launch_date": "-launch_date", "created_at": "created_at", "-created_at": "-created_at", "updated_at": "updated_at", "-updated_at": "-updated_at", "transponder_count": "transponder_count", "-transponder_count": "-transponder_count", } if sort_param in valid_sort_fields: satellites = satellites.order_by(valid_sort_fields[sort_param]) # Create paginator paginator = Paginator(satellites, items_per_page) page_obj = paginator.get_page(page_number) # Prepare data for display processed_satellites = [] for satellite in page_obj: # Get band names band_names = [band.name for band in satellite.band.all()] processed_satellites.append({ 'id': satellite.id, 'name': satellite.name or "-", 'norad': satellite.norad if satellite.norad else "-", 'bands': ", ".join(band_names) if band_names else "-", 'undersat_point': f"{satellite.undersat_point:.2f}" if satellite.undersat_point is not None else "-", 'launch_date': satellite.launch_date.strftime("%d.%m.%Y") if satellite.launch_date else "-", 'url': satellite.url or "-", 'comment': satellite.comment or "-", 'transponder_count': satellite.transponder_count, 'created_at': satellite.created_at, 'updated_at': satellite.updated_at, 'created_by': satellite.created_by if satellite.created_by else "-", 'updated_by': satellite.updated_by if satellite.updated_by else "-", }) # Prepare context for template context = { 'page_obj': page_obj, 'processed_satellites': processed_satellites, 'items_per_page': items_per_page, 'available_items_per_page': [50, 100, 500, 1000], 'sort': sort_param, 'search_query': search_query, 'bands': bands, 'selected_bands': [ int(x) if isinstance(x, str) else x for x in selected_bands if (isinstance(x, int) or (isinstance(x, str) and x.isdigit())) ], 'norad_min': norad_min, 'norad_max': norad_max, 'undersat_point_min': undersat_point_min, 'undersat_point_max': undersat_point_max, 'launch_date_from': launch_date_from, 'launch_date_to': launch_date_to, 'date_from': date_from, 'date_to': date_to, 'full_width_page': True, } return render(request, "mainapp/satellite_list.html", context) class SatelliteCreateView(RoleRequiredMixin, FormMessageMixin, CreateView): """View for creating a new satellite.""" model = Satellite form_class = SatelliteForm template_name = "mainapp/satellite_form.html" success_url = reverse_lazy("mainapp:satellite_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 SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView): """View for updating an existing satellite.""" model = Satellite form_class = SatelliteForm template_name = "mainapp/satellite_form.html" success_url = reverse_lazy("mainapp:satellite_list") success_message = "Спутник успешно обновлен!" required_roles = ["admin", "moderator"] def get_context_data(self, **kwargs): import json context = super().get_context_data(**kwargs) context['action'] = 'update' context['title'] = f'Редактирование спутника: {self.object.name}' # Get transponders for this satellite for frequency plan from mapsapp.models import Transponders transponders = Transponders.objects.filter( sat_id=self.object ).select_related('polarization').order_by('downlink') # Prepare transponder data for frequency plan visualization transponder_data = [] for t in transponders: if t.downlink and t.frequency_range: transponder_data.append({ 'id': t.id, 'name': t.name or f"TP-{t.id}", 'downlink': float(t.downlink), 'uplink': float(t.uplink) if t.uplink else None, 'frequency_range': float(t.frequency_range), 'polarization': t.polarization.name if t.polarization else '-', 'zone_name': t.zone_name or '-', }) context['transponders'] = json.dumps(transponder_data) context['transponder_count'] = len(transponder_data) return context def form_valid(self, form): form.instance.updated_by = self.request.user.customuser return super().form_valid(form) class DeleteSelectedSatellitesView(RoleRequiredMixin, View): """View for deleting multiple selected satellites with confirmation.""" required_roles = ["admin", "moderator"] def get(self, request): """Show confirmation page with details about satellites to be deleted.""" ids = request.GET.get("ids", "") if not ids: messages.error(request, "Не выбраны спутники для удаления") return redirect('mainapp:satellite_list') try: id_list = [int(x) for x in ids.split(",") if x.isdigit()] satellites = Satellite.objects.filter(id__in=id_list).prefetch_related( 'band' ).annotate( transponder_count=Count('tran_satellite', distinct=True) ) # Prepare detailed information about satellites satellites_info = [] total_transponders = 0 for satellite in satellites: transponder_count = satellite.transponder_count total_transponders += transponder_count satellites_info.append({ 'id': satellite.id, 'name': satellite.name or "-", 'norad': satellite.norad if satellite.norad else "-", 'transponder_count': transponder_count, }) context = { 'satellites_info': satellites_info, 'total_satellites': len(satellites_info), 'total_transponders': total_transponders, 'ids': ids, } return render(request, 'mainapp/satellite_bulk_delete_confirm.html', context) except Exception as e: messages.error(request, f'Ошибка при подготовке удаления: {str(e)}') return redirect('mainapp:satellite_list') def post(self, request): """Actually delete the selected satellites.""" 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 satellites = Satellite.objects.filter(id__in=id_list) deleted_count = satellites.count() # Delete satellites (cascade will handle related objects) satellites.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)