""" Представления для страницы Кубсат с фильтрацией и экспортом в Excel """ from datetime import datetime from io import BytesIO from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.gis.geos import Point from django.db.models import Count, Q from django.http import HttpResponse from django.views.generic import FormView from openpyxl import Workbook from openpyxl.styles import Font, Alignment from mainapp.forms import KubsatFilterForm from mainapp.models import Source, ObjItem from mainapp.permissions import PermissionRequiredMixin from mainapp.utils import calculate_mean_coords class KubsatView(LoginRequiredMixin, PermissionRequiredMixin, FormView): """Страница Кубсат с фильтрами и таблицей источников""" permission_required = 'kubsat_view' template_name = 'mainapp/kubsat_tabs.html' form_class = KubsatFilterForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['full_width_page'] = True # Добавляем данные для вкладки заявок from mainapp.models import SourceRequest, Satellite # Список спутников для формы создания заявки context['satellites'] = Satellite.objects.all().order_by('name') requests_qs = SourceRequest.objects.select_related( 'source', 'source__info', 'source__ownership', 'satellite', 'created_by__user', 'updated_by__user' ).prefetch_related( 'source__source_objitems__parameter_obj__modulation' ).order_by('-created_at') # Фильтры для заявок status = self.request.GET.get('status') if status: requests_qs = requests_qs.filter(status=status) priority = self.request.GET.get('priority') if priority: requests_qs = requests_qs.filter(priority=priority) # Добавляем данные источника к каждой заявке requests_list = [] for req in requests_qs[:100]: # Получаем данные из первой точки источника objitem_name = '-' modulation = '-' symbol_rate = '-' if req.source: first_objitem = req.source.source_objitems.select_related( 'parameter_obj__modulation' ).order_by('geo_obj__timestamp').first() if first_objitem: objitem_name = first_objitem.name or '-' if first_objitem.parameter_obj: if first_objitem.parameter_obj.modulation: modulation = first_objitem.parameter_obj.modulation.name if first_objitem.parameter_obj.bod_velocity and first_objitem.parameter_obj.bod_velocity > 0: symbol_rate = str(int(first_objitem.parameter_obj.bod_velocity)) # Добавляем атрибуты к объекту заявки req.objitem_name = objitem_name req.modulation = modulation req.symbol_rate = symbol_rate requests_list.append(req) context['requests'] = requests_list # Сериализуем заявки в JSON для Tabulator import json from django.utils import timezone requests_json_data = [] for req in requests_list: # Конвертируем даты в локальный часовой пояс для отображения planned_at_local = None planned_at_iso = None if req.planned_at: planned_at_local = timezone.localtime(req.planned_at) planned_at_iso = planned_at_local.isoformat() requests_json_data.append({ 'id': req.id, 'source_id': req.source_id, 'satellite_name': req.satellite.name if req.satellite else '-', 'status': req.status, 'status_display': req.get_status_display(), 'priority': req.priority, 'priority_display': req.get_priority_display(), # Даты в ISO формате для правильной сортировки 'request_date': req.request_date.isoformat() if req.request_date else None, 'card_date': req.card_date.isoformat() if req.card_date else None, 'planned_at': planned_at_iso, # Отформатированные даты для отображения 'request_date_display': req.request_date.strftime('%d.%m.%Y') if req.request_date else '-', 'card_date_display': req.card_date.strftime('%d.%m.%Y') if req.card_date else '-', 'planned_at_display': ( planned_at_local.strftime('%d.%m.%Y') if planned_at_local and planned_at_local.hour == 0 and planned_at_local.minute == 0 else planned_at_local.strftime('%d.%m.%Y %H:%M') if planned_at_local else '-' ), 'downlink': float(req.downlink) if req.downlink else None, 'uplink': float(req.uplink) if req.uplink else None, 'transfer': float(req.transfer) if req.transfer else None, 'coords_lat': float(req.coords.y) if req.coords else None, 'coords_lon': float(req.coords.x) if req.coords else None, 'region': req.region or '', 'gso_success': req.gso_success, 'kubsat_success': req.kubsat_success, 'coords_source_lat': float(req.coords_source.y) if req.coords_source else None, 'coords_source_lon': float(req.coords_source.x) if req.coords_source else None, 'coords_object_lat': float(req.coords_object.y) if req.coords_object else None, 'coords_object_lon': float(req.coords_object.x) if req.coords_object else None, 'comment': req.comment or '', }) context['requests_json'] = json.dumps(requests_json_data, ensure_ascii=False) context['status_choices'] = SourceRequest.STATUS_CHOICES context['priority_choices'] = SourceRequest.PRIORITY_CHOICES context['current_status'] = status or '' context['current_priority'] = priority or '' context['search_query'] = self.request.GET.get('search', '') # Если форма была отправлена, применяем фильтры if self.request.GET: form = self.form_class(self.request.GET) if form.is_valid(): sources = self.apply_filters(form.cleaned_data) date_from = form.cleaned_data.get('date_from') date_to = form.cleaned_data.get('date_to') has_date_filter = bool(date_from or date_to) objitem_count_min = form.cleaned_data.get('objitem_count_min') objitem_count_max = form.cleaned_data.get('objitem_count_max') sources_with_date_info = [] for source in sources: # Get latest request info for this source latest_request = source.source_requests.order_by('-created_at').first() requests_count = source.source_requests.count() source_data = { 'source': source, 'objitems_data': [], 'has_lyngsat': False, 'lyngsat_id': None, 'has_request': latest_request is not None, 'request_status': latest_request.get_status_display() if latest_request else None, 'request_status_raw': latest_request.status if latest_request else None, 'gso_success': latest_request.gso_success if latest_request else None, 'kubsat_success': latest_request.kubsat_success if latest_request else None, 'planned_at': latest_request.planned_at if latest_request else None, 'requests_count': requests_count, 'average_coords': None, # Будет рассчитано после сбора точек } for objitem in source.source_objitems.all(): # Check if objitem has LyngSat source if hasattr(objitem, 'lyngsat_source') and objitem.lyngsat_source: source_data['has_lyngsat'] = True source_data['lyngsat_id'] = objitem.lyngsat_source.id objitem_matches_date = True objitem_matches_date = True geo_date = None if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp: geo_date = objitem.geo_obj.timestamp.date() # Проверяем попадание в диапазон дат (только если фильтр задан) if has_date_filter: if date_from and date_to: objitem_matches_date = date_from <= geo_date <= date_to elif date_from: objitem_matches_date = geo_date >= date_from elif date_to: objitem_matches_date = geo_date <= date_to elif has_date_filter: # Если фильтр по дате задан, но у точки нет даты - не подходит objitem_matches_date = False # Добавляем только точки, подходящие по дате (или все, если фильтр не задан) if not has_date_filter or objitem_matches_date: source_data['objitems_data'].append({ 'objitem': objitem, 'matches_date': objitem_matches_date, 'geo_date': geo_date }) # ЭТАП 2: Проверяем количество отфильтрованных точек filtered_count = len(source_data['objitems_data']) # Применяем фильтр по количеству точек (если задан) include_source = True if objitem_count_min is not None and filtered_count < objitem_count_min: include_source = False if objitem_count_max is not None and filtered_count > objitem_count_max: include_source = False # Сортируем точки по дате ГЛ перед расчётом усреднённых координат source_data['objitems_data'].sort( key=lambda x: x['geo_date'] if x['geo_date'] else datetime.min.date() ) # Рассчитываем усреднённые координаты из отфильтрованных точек if source_data['objitems_data']: avg_coords = None for objitem_info in source_data['objitems_data']: objitem = objitem_info['objitem'] if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords: coord = (float(objitem.geo_obj.coords.x), float(objitem.geo_obj.coords.y)) if avg_coords is None: avg_coords = coord else: avg_coords, _ = calculate_mean_coords(avg_coords, coord) if avg_coords: source_data['average_coords'] = avg_coords source_data['avg_lat'] = avg_coords[1] source_data['avg_lon'] = avg_coords[0] if source_data['objitems_data'] and include_source: sources_with_date_info.append(source_data) context['sources_with_date_info'] = sources_with_date_info context['form'] = form return context def apply_filters(self, filters): """Применяет фильтры к queryset Source""" from mainapp.models import SourceRequest from django.db.models import Subquery, OuterRef, Exists queryset = Source.objects.select_related('info', 'ownership').prefetch_related( 'source_objitems__parameter_obj__id_satellite', 'source_objitems__parameter_obj__polarization', 'source_objitems__parameter_obj__modulation', 'source_objitems__transponder__sat_id', 'source_objitems__lyngsat_source', 'source_objitems__geo_obj', 'source_requests' ).annotate(objitem_count=Count('source_objitems')) # Фильтр по спутникам if filters.get('satellites'): queryset = queryset.filter( source_objitems__parameter_obj__id_satellite__in=filters['satellites'] ).distinct() # Фильтр по полосе спутника if filters.get('band'): queryset = queryset.filter( source_objitems__parameter_obj__id_satellite__band__in=filters['band'] ).distinct() # Фильтр по поляризации if filters.get('polarization'): queryset = queryset.filter( source_objitems__parameter_obj__polarization__in=filters['polarization'] ).distinct() # Фильтр по центральной частоте if filters.get('frequency_min'): queryset = queryset.filter( source_objitems__parameter_obj__frequency__gte=filters['frequency_min'] ) if filters.get('frequency_max'): queryset = queryset.filter( source_objitems__parameter_obj__frequency__lte=filters['frequency_max'] ) # Фильтр по полосе частот if filters.get('freq_range_min'): queryset = queryset.filter( source_objitems__parameter_obj__freq_range__gte=filters['freq_range_min'] ) if filters.get('freq_range_max'): queryset = queryset.filter( source_objitems__parameter_obj__freq_range__lte=filters['freq_range_max'] ) # Фильтр по модуляции if filters.get('modulation'): queryset = queryset.filter( source_objitems__parameter_obj__modulation__in=filters['modulation'] ).distinct() # Фильтр по типу объекта if filters.get('object_type'): queryset = queryset.filter(info__in=filters['object_type']) # Фильтр по принадлежности объекта if filters.get('object_ownership'): queryset = queryset.filter(ownership__in=filters['object_ownership']) # Фильтр по количеству ObjItem (диапазон) objitem_count_min = filters.get('objitem_count_min') objitem_count_max = filters.get('objitem_count_max') if objitem_count_min is not None: queryset = queryset.filter(objitem_count__gte=objitem_count_min) if objitem_count_max is not None: queryset = queryset.filter(objitem_count__lte=objitem_count_max) # Фильтр по наличию планов (заявок со статусом 'planned') has_plans = filters.get('has_plans') if has_plans == 'yes': queryset = queryset.filter( source_requests__status='planned' ).distinct() elif has_plans == 'no': queryset = queryset.exclude( source_requests__status='planned' ).distinct() # Фильтр по ГСО успешно success_1 = filters.get('success_1') if success_1 == 'yes': queryset = queryset.filter( source_requests__gso_success=True ).distinct() elif success_1 == 'no': queryset = queryset.filter( source_requests__gso_success=False ).distinct() # Фильтр по Кубсат успешно success_2 = filters.get('success_2') if success_2 == 'yes': queryset = queryset.filter( source_requests__kubsat_success=True ).distinct() elif success_2 == 'no': queryset = queryset.filter( source_requests__kubsat_success=False ).distinct() return queryset.distinct() class KubsatExportView(LoginRequiredMixin, PermissionRequiredMixin, FormView): """Экспорт отфильтрованных данных в Excel""" permission_required = 'kubsat_view' form_class = KubsatFilterForm def post(self, request, *args, **kwargs): # Получаем список ID точек (ObjItem) из POST objitem_ids = request.POST.getlist('objitem_ids') if not objitem_ids: return HttpResponse("Нет данных для экспорта", status=400) # Получаем ObjItem с их источниками objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related( 'source', 'source__info', 'parameter_obj__id_satellite', 'parameter_obj__polarization', 'transponder__sat_id', 'geo_obj' ).prefetch_related('geo_obj__mirrors') # Группируем ObjItem по Source для расчета инкрементального среднего sources_objitems = {} for objitem in objitems: if objitem.source: if objitem.source.id not in sources_objitems: sources_objitems[objitem.source.id] = { 'source': objitem.source, 'objitems': [] } sources_objitems[objitem.source.id]['objitems'].append(objitem) # Создаем Excel файл с двумя листами wb = Workbook() # Первый лист: "Предложения" (только основные данные) ws_proposals = wb.active ws_proposals.title = "Предложения" # Заголовки для листа "Предложения" headers_proposals = [ 'Дата', 'Широта, град', 'Долгота, град', 'Высота, м', 'Местоположение', 'ИСЗ', 'Прямой канал, МГц', 'Обратный канал, МГц', 'Перенос' ] # Стиль заголовков для листа "Предложения" for col_num, header in enumerate(headers_proposals, 1): cell = ws_proposals.cell(row=1, column=col_num, value=header) cell.font = Font(bold=True) cell.alignment = Alignment(horizontal='center', vertical='center') # Второй лист: "Комментарий" (все данные) ws_comments = wb.create_sheet(title="Комментарий") # Заголовки для листа "Комментарий" headers_comments = [ 'Дата', 'Широта, град', 'Долгота, град', 'Высота, м', 'Местоположение', 'ИСЗ', 'Прямой канал, МГц', 'Обратный канал, МГц', 'Перенос', 'Получено координат, раз', 'Период получения координат', 'Зеркала', 'СКО, км', 'Примечание', 'Оператор' ] # Стиль заголовков для листа "Комментарий" for col_num, header in enumerate(headers_comments, 1): cell = ws_comments.cell(row=1, column=col_num, value=header) cell.font = Font(bold=True) cell.alignment = Alignment(horizontal='center', vertical='center') # Заполняем данные current_date = datetime.now().strftime('%d.%m.%Y') operator_name = f"{request.user.first_name} {request.user.last_name}" if request.user.first_name else request.user.username row_num_proposals = 2 row_num_comments = 2 for source_id, data in sources_objitems.items(): source = data['source'] objitems_list = data['objitems'] # Сортируем точки по дате ГЛ перед расчётом objitems_list.sort( key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min ) # Рассчитываем инкрементальное среднее координат из оставшихся точек average_coords = None for objitem in objitems_list: if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords: coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y) if average_coords is None: # Первая точка average_coords = coord else: # Инкрементальное усреднение average_coords, _ = calculate_mean_coords(average_coords, coord) # Если нет координат из geo_obj, берем из source if average_coords is None: coords = source.coords_kupsat or source.coords_average or source.coords_valid or source.coords_reference if coords: average_coords = (coords.x, coords.y) latitude = average_coords[1] if average_coords else '' longitude = average_coords[0] if average_coords else '' # Получаем местоположение из первого ObjItem с geo_obj location = '' for objitem in objitems_list: if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.location: location = objitem.geo_obj.location break # Получаем данные спутника и частоты satellite_info = '' reverse_channel = '' direct_channel = '' transfer = '' for objitem in objitems_list: if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj: param = objitem.parameter_obj if param.id_satellite: sat_name = param.id_satellite.name norad = f"({param.id_satellite.norad})" if param.id_satellite.norad else "" satellite_info = f"{sat_name} {norad}" if param.frequency: reverse_channel = param.frequency if objitem.transponder and objitem.transponder.transfer: transfer = objitem.transponder.transfer if param.frequency: direct_channel = param.frequency + objitem.transponder.transfer break objitem_count = len(objitems_list) # Зеркала mirrors = [] for objitem in objitems_list: if hasattr(objitem, 'geo_obj') and objitem.geo_obj: for mirror in objitem.geo_obj.mirrors.all(): if mirror.name not in mirrors: mirrors.append(mirror.name) mirrors_str = '\n'.join(mirrors) # Диапазон дат ГЛ (самая ранняя - самая поздняя) geo_dates = [] for objitem in objitems_list: if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp: geo_dates.append(objitem.geo_obj.timestamp.date()) date_range_str = '-' if geo_dates: min_date = min(geo_dates) max_date = max(geo_dates) # Форматируем даты в формате d.m.Y min_date_str = min_date.strftime('%d.%m.%Y') max_date_str = max_date.strftime('%d.%m.%Y') if min_date == max_date: # Если даты совпадают, показываем только одну date_range_str = min_date_str else: # Иначе показываем диапазон date_range_str = f"{min_date_str}-{max_date_str}" # Записываем строку на лист "Предложения" (только основные данные) ws_proposals.cell(row=row_num_proposals, column=1, value=current_date) ws_proposals.cell(row=row_num_proposals, column=2, value=latitude) ws_proposals.cell(row=row_num_proposals, column=3, value=longitude) ws_proposals.cell(row=row_num_proposals, column=4, value=0.0) ws_proposals.cell(row=row_num_proposals, column=5, value=location) ws_proposals.cell(row=row_num_proposals, column=6, value=satellite_info) ws_proposals.cell(row=row_num_proposals, column=7, value=direct_channel) ws_proposals.cell(row=row_num_proposals, column=8, value=reverse_channel) ws_proposals.cell(row=row_num_proposals, column=9, value=transfer) # Записываем строку на лист "Комментарий" (все данные) ws_comments.cell(row=row_num_comments, column=1, value=current_date) ws_comments.cell(row=row_num_comments, column=2, value=latitude) ws_comments.cell(row=row_num_comments, column=3, value=longitude) ws_comments.cell(row=row_num_comments, column=4, value=0.0) ws_comments.cell(row=row_num_comments, column=5, value=location) ws_comments.cell(row=row_num_comments, column=6, value=satellite_info) ws_comments.cell(row=row_num_comments, column=7, value=direct_channel) ws_comments.cell(row=row_num_comments, column=8, value=reverse_channel) ws_comments.cell(row=row_num_comments, column=9, value=transfer) ws_comments.cell(row=row_num_comments, column=10, value=objitem_count) ws_comments.cell(row=row_num_comments, column=11, value=date_range_str) ws_comments.cell(row=row_num_comments, column=12, value=mirrors_str) ws_comments.cell(row=row_num_comments, column=13, value='') ws_comments.cell(row=row_num_comments, column=14, value='') ws_comments.cell(row=row_num_comments, column=15, value=operator_name) row_num_proposals += 1 row_num_comments += 1 # Автоширина колонок для обоих листов for ws in [ws_proposals, ws_comments]: for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) ws.column_dimensions[column_letter].width = adjusted_width # Сохраняем в BytesIO output = BytesIO() wb.save(output) output.seek(0) # Возвращаем файл response = HttpResponse( output.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = f'attachment; filename="kubsat_{datetime.now().strftime("%Y%m%d")}.xlsx"' return response class KubsatCreateRequestsView(LoginRequiredMixin, PermissionRequiredMixin, FormView): """Массовое создание заявок из отфильтрованных данных""" permission_required = 'request_create' form_class = KubsatFilterForm def post(self, request, *args, **kwargs): import json from django.http import JsonResponse from mainapp.models import SourceRequest, CustomUser # Получаем список ID точек (ObjItem) из POST objitem_ids = request.POST.getlist('objitem_ids') if not objitem_ids: return JsonResponse({'success': False, 'error': 'Нет данных для создания заявок'}, status=400) # Получаем ObjItem с их источниками objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related( 'source', 'geo_obj' ) # Группируем ObjItem по Source sources_objitems = {} for objitem in objitems: if objitem.source: if objitem.source.id not in sources_objitems: sources_objitems[objitem.source.id] = { 'source': objitem.source, 'objitems': [] } sources_objitems[objitem.source.id]['objitems'].append(objitem) # Получаем CustomUser для текущего пользователя try: custom_user = CustomUser.objects.get(user=request.user) except CustomUser.DoesNotExist: custom_user = None created_count = 0 errors = [] for source_id, data in sources_objitems.items(): source = data['source'] objitems_list = data['objitems'] # Сортируем точки по дате ГЛ перед расчётом objitems_list.sort( key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min ) # Рассчитываем усреднённые координаты из выбранных точек average_coords = None points_with_coords = 0 for objitem in objitems_list: if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords: coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y) points_with_coords += 1 if average_coords is None: average_coords = coord else: average_coords, _ = calculate_mean_coords(average_coords, coord) # Создаём Point объект если есть координаты coords_point = None if average_coords: coords_point = Point(average_coords[0], average_coords[1], srid=4326) try: # Создаём новую заявку со статусом "planned" source_request = SourceRequest.objects.create( source=source, status='planned', priority='medium', coords=coords_point, points_count=points_with_coords, created_by=custom_user, updated_by=custom_user, comment=f'Создано из Кубсат. Точек: {len(objitems_list)}' ) created_count += 1 except Exception as e: errors.append(f'Источник #{source_id}: {str(e)}') return JsonResponse({ 'success': True, 'created_count': created_count, 'total_sources': len(sources_objitems), 'errors': errors }) class KubsatRecalculateCoordsView(LoginRequiredMixin, PermissionRequiredMixin, FormView): """API для пересчёта усреднённых координат по списку ObjItem ID""" permission_required = 'kubsat_view' form_class = KubsatFilterForm def post(self, request, *args, **kwargs): import json from django.http import JsonResponse # Получаем список ID точек (ObjItem) из POST objitem_ids = request.POST.getlist('objitem_ids') if not objitem_ids: return JsonResponse({'success': False, 'error': 'Нет данных для расчёта'}, status=400) # Получаем ObjItem с их источниками, сортируем по дате ГЛ objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related( 'source', 'geo_obj' ).order_by('geo_obj__timestamp') # Сортировка по дате ГЛ # Группируем ObjItem по Source sources_objitems = {} for objitem in objitems: if objitem.source: if objitem.source.id not in sources_objitems: sources_objitems[objitem.source.id] = [] sources_objitems[objitem.source.id].append(objitem) # Рассчитываем усреднённые координаты для каждого источника results = {} for source_id, objitems_list in sources_objitems.items(): # Сортируем по дате ГЛ (на случай если порядок сбился) objitems_list.sort(key=lambda x: x.geo_obj.timestamp if x.geo_obj and x.geo_obj.timestamp else datetime.min) average_coords = None points_count = 0 for objitem in objitems_list: if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords: coord = (float(objitem.geo_obj.coords.x), float(objitem.geo_obj.coords.y)) points_count += 1 if average_coords is None: average_coords = coord else: average_coords, _ = calculate_mean_coords(average_coords, coord) if average_coords: results[str(source_id)] = { 'avg_lon': average_coords[0], 'avg_lat': average_coords[1], 'points_count': points_count } else: results[str(source_id)] = { 'avg_lon': None, 'avg_lat': None, 'points_count': 0 } return JsonResponse({ 'success': True, 'results': results })