""" 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 ..permissions import PermissionRequiredMixin 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 - default to "Все" (all items) for satellites # If no items_per_page is specified, use MAX_ITEMS_PER_PAGE from ..utils import MAX_ITEMS_PER_PAGE default_per_page = MAX_ITEMS_PER_PAGE if not request.GET.get("items_per_page") else None if default_per_page: page_number, items_per_page = parse_pagination_params(request, default_per_page=default_per_page) else: 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") selected_location_places = request.GET.getlist("location_place") 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() transponder_count_min = request.GET.get("transponder_count_min", "").strip() transponder_count_max = request.GET.get("transponder_count_max", "").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 location_place if selected_location_places: satellites = satellites.filter(location_place__in=selected_location_places) # 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, alternative_name, or comment if search_query: satellites = satellites.filter( Q(name__icontains=search_query) | Q(alternative_name__icontains=search_query) | Q(comment__icontains=search_query) ) # Filter by transponder count if transponder_count_min: try: min_val = int(transponder_count_min) satellites = satellites.filter(transponder_count__gte=min_val) except ValueError: pass if transponder_count_max: try: max_val = int(transponder_count_max) satellites = satellites.filter(transponder_count__lte=max_val) except ValueError: pass # Apply sorting valid_sort_fields = { "id": "id", "-id": "-id", "name": "name", "-name": "-name", "alternative_name": "alternative_name", "-alternative_name": "-alternative_name", "norad": "norad", "-norad": "-norad", "international_code": "international_code", "-international_code": "-international_code", "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", "location_place": "location_place", "-location_place": "-location_place", } 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()] # Get location_place display value location_place_display = dict(Satellite.PLACES).get(satellite.location_place, "-") if satellite.location_place else "-" processed_satellites.append({ 'id': satellite.id, 'name': satellite.name or "-", 'alternative_name': satellite.alternative_name or "-", 'location_place': location_place_display, 'norad': satellite.norad if satellite.norad else "-", 'international_code': satellite.international_code or "-", '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())) ], 'location_places': Satellite.PLACES, 'selected_location_places': selected_location_places, '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, 'transponder_count_min': transponder_count_min, 'transponder_count_max': transponder_count_max, 'full_width_page': True, } return render(request, "mainapp/satellite_list.html", context) class SatelliteCreateView(PermissionRequiredMixin, FormMessageMixin, CreateView): """View for creating a new satellite.""" permission_required = 'satellite_create' model = Satellite form_class = SatelliteForm template_name = "mainapp/satellite_form.html" success_url = reverse_lazy("mainapp:satellite_list") success_message = "Спутник успешно создан!" 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(PermissionRequiredMixin, FormMessageMixin, UpdateView): """View for updating an existing satellite.""" permission_required = 'satellite_edit' model = Satellite form_class = SatelliteForm template_name = "mainapp/satellite_form.html" success_url = reverse_lazy("mainapp:satellite_list") success_message = "Спутник успешно обновлен!" 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(PermissionRequiredMixin, View): """View for deleting multiple selected satellites with confirmation.""" permission_required = 'satellite_delete' 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)