diff --git a/dbapp/mainapp/templates/mainapp/source_list.html b/dbapp/mainapp/templates/mainapp/source_list.html index a4e069c..bee7275 100644 --- a/dbapp/mainapp/templates/mainapp/source_list.html +++ b/dbapp/mainapp/templates/mainapp/source_list.html @@ -1620,6 +1620,19 @@ function showPlaybackAnimation() { window.open(url, '_blank'); } +// Function to show source marks for selected sources +function showSourceMarks() { + if (!window.selectedSources || window.selectedSources.length === 0) { + alert('Список источников пуст'); + return; + } + + const selectedIds = window.selectedSources.map(source => source.id); + const url = '{% url "mainapp:source_marks" %}' + '?ids=' + selectedIds.join(','); + + window.open(url, '_blank'); +} + // Function to merge selected sources function mergeSelectedSources() { if (!window.selectedSources || window.selectedSources.length < 2) { @@ -2226,6 +2239,9 @@ function showTransponderModal(transponderId) { Анимация + + Отметки + {% if user|has_perm:'source_merge' %} Объединить diff --git a/dbapp/mainapp/templates/mainapp/source_marks.html b/dbapp/mainapp/templates/mainapp/source_marks.html new file mode 100644 index 0000000..82b4b54 --- /dev/null +++ b/dbapp/mainapp/templates/mainapp/source_marks.html @@ -0,0 +1,483 @@ +{% extends "mainapp/base.html" %} +{% load static %} + +{% block title %}Отметки сигналов по источникам{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + + + + + Отметки сигналов по источникам + {{ source_ids|length }} источников + + + Назад к списку + + + + + + + + Поиск (имя или частота): + + + + + + Обновить + + + Фильтры 0 + + + + Колонки + + + Выбрать всё + + Имя + Координаты + Спутник + Частота + Полоса + Поляризация + Модуляция + Бодовая скорость + + + + + + + + + + + + + + + + Загрузка... + + Загрузка данных... + + + + + + + + + + Фильтры + + + + + + Поляризация: + + Все + Снять + + + {% for pol in polarizations %} + {{ pol.name }} + {% endfor %} + + + + + + Модуляция: + + Все + Снять + + + {% for mod in modulations %} + {{ mod.name }} + {% endfor %} + + + + + + Частота (МГц): + + + + + + + + + Полоса (МГц): + + + + + + + + + Бодовая скорость: + + + + + + + + + Применить фильтры + + + Очистить фильтры + + + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py index 4fec709..afa0fbb 100644 --- a/dbapp/mainapp/urls.py +++ b/dbapp/mainapp/urls.py @@ -72,6 +72,10 @@ from .views.marks import ( AddObjectMarkView, UpdateObjectMarkView, ) +from .views.source_marks import ( + SourceMarksView, + SourceMarksAPIView, +) from .views.source_requests import ( SourceRequestListView, SourceRequestCreateView, @@ -178,6 +182,9 @@ urlpatterns = [ path('object-marks/', ObjectMarksListView.as_view(), name='object_marks'), path('api/add-object-mark/', AddObjectMarkView.as_view(), name='add_object_mark'), path('api/update-object-mark/', UpdateObjectMarkView.as_view(), name='update_object_mark'), + # Source Marks (отметки по выбранным источникам) + path('source-marks/', SourceMarksView.as_view(), name='source_marks'), + path('api/source-marks/', SourceMarksAPIView.as_view(), name='source_marks_api'), path('kubsat/', KubsatView.as_view(), name='kubsat'), path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'), path('kubsat/create-requests/', KubsatCreateRequestsView.as_view(), name='kubsat_create_requests'), diff --git a/dbapp/mainapp/views/source_marks.py b/dbapp/mainapp/views/source_marks.py new file mode 100644 index 0000000..d087b40 --- /dev/null +++ b/dbapp/mainapp/views/source_marks.py @@ -0,0 +1,301 @@ +""" +Views для отображения отметок сигналов по выбранным источникам. +Сопоставляет теханализы с первой точкой источника по имени. +""" + +import json +from datetime import timedelta + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Max, Min, Q +from django.http import JsonResponse +from django.shortcuts import render +from django.utils import timezone +from django.views import View + +from mainapp.models import ( + Source, + ObjItem, + TechAnalyze, + ObjectMark, + Polarization, + Modulation, +) + + +class SourceMarksView(LoginRequiredMixin, View): + """ + Страница отображения отметок сигналов для выбранных источников. + Сопоставляет теханализы с первой точкой источника по имени. + """ + + def get(self, request): + # Получаем IDs источников из параметра + ids_param = request.GET.get('ids', '') + source_ids = [int(id_str) for id_str in ids_param.split(',') if id_str.strip().isdigit()] + + # Справочники для фильтров + polarizations = Polarization.objects.all().order_by('name') + modulations = Modulation.objects.all().order_by('name') + + context = { + 'source_ids': source_ids, + 'source_ids_json': json.dumps(source_ids), + 'full_width_page': True, + 'polarizations': polarizations, + 'modulations': modulations, + } + + return render(request, 'mainapp/source_marks.html', context) + + +class SourceMarksAPIView(LoginRequiredMixin, View): + """ + API для получения данных отметок по выбранным источникам. + Сопоставляет теханализы с первой точкой источника по имени. + """ + + def get(self, request): + from datetime import datetime + + # Получаем параметры + ids_param = request.GET.get('ids', '') + source_ids = [int(id_str) for id_str in ids_param.split(',') if id_str.strip().isdigit()] + + size = int(request.GET.get('size', 0)) + search = request.GET.get('search', '').strip() + + # Фильтры + polarization_ids = request.GET.getlist('polarization_id') + modulation_ids = request.GET.getlist('modulation_id') + freq_min = request.GET.get('freq_min') + freq_max = request.GET.get('freq_max') + freq_range_min = request.GET.get('freq_range_min') + freq_range_max = request.GET.get('freq_range_max') + bod_velocity_min = request.GET.get('bod_velocity_min') + bod_velocity_max = request.GET.get('bod_velocity_max') + + if not source_ids: + return JsonResponse({ + 'error': 'Не выбраны источники', + 'periods': [], + 'data': [], + }, status=400) + + # Получаем источники с их первыми точками + sources = Source.objects.filter(id__in=source_ids).prefetch_related('source_objitems') + + if not sources.exists(): + return JsonResponse({ + 'message': 'Источники не найдены', + 'periods': [], + 'data': [], + }) + + # Собираем имена первых точек для каждого источника + source_first_objitem_names = {} + source_coords = {} + + for source in sources: + # Получаем первую точку источника (по ID - порядок добавления) + first_objitem = source.source_objitems.order_by('id').first() + if first_objitem and first_objitem.name: + source_first_objitem_names[source.id] = first_objitem.name + + # Сохраняем усреднённые координаты + if source.coords_average: + source_coords[source.id] = f"{source.coords_average.y:.6f}, {source.coords_average.x:.6f}" + else: + source_coords[source.id] = "-" + + if not source_first_objitem_names: + return JsonResponse({ + 'message': 'У выбранных источников нет точек с именами', + 'periods': [], + 'data': [], + }) + + # Получаем имена для поиска теханализов + objitem_names = list(source_first_objitem_names.values()) + + # Ищем теханализы по именам + tech_analyzes = TechAnalyze.objects.filter( + name__in=objitem_names + ).select_related( + 'satellite', 'polarization', 'modulation', 'standard' + ).order_by('frequency', 'name') + + if not tech_analyzes.exists(): + return JsonResponse({ + 'message': 'Не найдены теханализы, соответствующие именам точек выбранных источников', + 'periods': [], + 'data': [], + }) + + # Применяем фильтры к теханализам + if polarization_ids: + tech_analyzes = tech_analyzes.filter(polarization_id__in=polarization_ids) + if modulation_ids: + tech_analyzes = tech_analyzes.filter(modulation_id__in=modulation_ids) + if freq_min: + try: + tech_analyzes = tech_analyzes.filter(frequency__gte=float(freq_min)) + except ValueError: + pass + if freq_max: + try: + tech_analyzes = tech_analyzes.filter(frequency__lte=float(freq_max)) + except ValueError: + pass + if freq_range_min: + try: + tech_analyzes = tech_analyzes.filter(freq_range__gte=float(freq_range_min)) + except ValueError: + pass + if freq_range_max: + try: + tech_analyzes = tech_analyzes.filter(freq_range__lte=float(freq_range_max)) + except ValueError: + pass + if bod_velocity_min: + try: + tech_analyzes = tech_analyzes.filter(bod_velocity__gte=float(bod_velocity_min)) + except ValueError: + pass + if bod_velocity_max: + try: + tech_analyzes = tech_analyzes.filter(bod_velocity__lte=float(bod_velocity_max)) + except ValueError: + pass + if search: + tech_analyzes = tech_analyzes.filter( + Q(name__icontains=search) | Q(id__icontains=search) + ) + + # Создаём маппинг имя теханализа -> source_id для координат + name_to_source_id = {name: sid for sid, name in source_first_objitem_names.items()} + + # Получаем ID теханализов + ta_ids = list(tech_analyzes.values_list('id', flat=True)) + + # Фильтруем отметки за последние 90 дней + date_90_days_ago = timezone.now() - timedelta(days=90) + marks_qs = ObjectMark.objects.filter( + tech_analyze_id__in=ta_ids, + timestamp__gte=date_90_days_ago + ).select_related('created_by__user', 'tech_analyze') + + # Получаем диапазон дат с отметками + date_range = marks_qs.aggregate( + min_date=Min('timestamp'), + max_date=Max('timestamp') + ) + + min_date = date_range['min_date'] + max_date = date_range['max_date'] + + if not min_date or not max_date: + # Нет отметок, но есть теханализы - показываем пустую таблицу + data = [] + for ta in tech_analyzes: + source_id = name_to_source_id.get(ta.name) + coords = source_coords.get(source_id, '-') if source_id else '-' + + data.append({ + 'id': ta.id, + 'name': ta.name, + 'frequency': float(ta.frequency) if ta.frequency else 0, + 'freq_range': float(ta.freq_range) if ta.freq_range else 0, + 'polarization': ta.polarization.name if ta.polarization else '-', + 'modulation': ta.modulation.name if ta.modulation else '-', + 'bod_velocity': float(ta.bod_velocity) if ta.bod_velocity else 0, + 'coords_average': coords, + 'satellite': ta.satellite.name if ta.satellite else '-', + 'marks': [], + }) + + return JsonResponse({ + 'periods': [], + 'data': data, + 'message': 'Нет отметок за последние 90 дней', + }) + + # Генерируем список дней от max_date до min_date (от новых к старым) + days = [] + current_date = max_date.date() + end_date = min_date.date() + while current_date >= end_date: + days.append(current_date) + current_date -= timedelta(days=1) + + # Формируем заголовки колонок (дни) + periods = [] + for day in days: + periods.append({ + 'date': day, + 'label': day.strftime('%d.%m'), + }) + + # Загружаем все отметки + all_marks = list(marks_qs.order_by('-timestamp')) + + # Создаём словарь: {(tech_analyze_id, date): список всех отметок за день} + marks_dict = {} + for mark in all_marks: + mark_date = timezone.localtime(mark.timestamp).date() + key = (mark.tech_analyze_id, mark_date) + if key not in marks_dict: + marks_dict[key] = [] + marks_dict[key].append(mark) + + # Формируем данные + data = [] + for ta in tech_analyzes: + source_id = name_to_source_id.get(ta.name) + coords = source_coords.get(source_id, '-') if source_id else '-' + + row = { + 'id': ta.id, + 'name': ta.name, + 'frequency': float(ta.frequency) if ta.frequency else 0, + 'freq_range': float(ta.freq_range) if ta.freq_range else 0, + 'polarization': ta.polarization.name if ta.polarization else '-', + 'modulation': ta.modulation.name if ta.modulation else '-', + 'bod_velocity': float(ta.bod_velocity) if ta.bod_velocity else 0, + 'coords_average': coords, + 'satellite': ta.satellite.name if ta.satellite else '-', + 'marks': [], + } + + # Для каждого дня собираем все отметки + for period in periods: + key = (ta.id, period['date']) + day_marks = marks_dict.get(key, []) + + if day_marks: + # Сортируем по времени (от раннего к позднему) + day_marks_sorted = sorted(day_marks, key=lambda m: m.timestamp) + + marks_list = [] + for mark in day_marks_sorted: + local_time = timezone.localtime(mark.timestamp) + marks_list.append({ + 'mark': mark.mark, + 'user': str(mark.created_by) if mark.created_by else '-', + 'time': local_time.strftime('%H:%M'), + }) + row['marks'].append({ + 'count': len(marks_list), + 'items': marks_list, + }) + else: + row['marks'].append(None) + + data.append(row) + + return JsonResponse({ + 'periods': [p['label'] for p in periods], + 'data': data, + 'total': len(data), + 'date_range': f"Последние 90 дней (с {date_90_days_ago.strftime('%d.%m.%Y')})", + })
Загрузка данных...