""" Представления для страницы Кубсат с фильтрацией и экспортом в 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.utils import calculate_mean_coords class KubsatView(LoginRequiredMixin, FormView): """Страница Кубсат с фильтрами и таблицей источников""" 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 requests_json_data = [] for req in requests_list: 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(), 'request_date': req.request_date.strftime('%d.%m.%Y') if req.request_date else '-', 'card_date': req.card_date.strftime('%d.%m.%Y') if req.card_date else '-', 'planned_at': req.planned_at.strftime('%d.%m.%Y %H:%M') if req.planned_at 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, '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 = form.cleaned_data.get('objitem_count') 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: if objitem_count == '1': include_source = (filtered_count == 1) elif objitem_count == '2+': include_source = (filtered_count >= 2) # Сортируем точки по дате ГЛ перед расчётом усреднённых координат 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 = filters.get('objitem_count') if objitem_count == '1': queryset = queryset.filter(objitem_count=1) elif objitem_count == '2+': queryset = queryset.filter(objitem_count__gte=2) # Фильтр по наличию планов (заявок со статусом '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, FormView): """Экспорт отфильтрованных данных в Excel""" 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, FormView): """Массовое создание заявок из отфильтрованных данных""" 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, FormView): """API для пересчёта усреднённых координат по списку ObjItem ID""" 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 })