Визуальные изменение. Доработки и фиксы багов

This commit is contained in:
2025-12-12 15:08:10 +03:00
parent f5875e5b87
commit 9bf701f05a
26 changed files with 1923 additions and 419 deletions

View File

@@ -3,13 +3,14 @@
"""
import json
from datetime import timedelta
from django.db.models import Count, Q, Min
from django.db.models.functions import TruncDate
from django.db.models import Count, Q, Min, Sum, F, Subquery, OuterRef
from django.db.models.functions import TruncDate, Abs
from django.utils import timezone
from django.views.generic import TemplateView
from django.http import JsonResponse
from ..models import ObjItem, Source, Satellite, Geo
from ..models import ObjItem, Source, Satellite, Geo, SourceRequest, SourceRequestStatusHistory
from mapsapp.models import Transponders
class StatisticsView(TemplateView):
@@ -209,6 +210,191 @@ class StatisticsView(TemplateView):
return list(daily)
def _get_zone_statistics(self, date_from, date_to, location_place):
"""
Получает статистику по зоне (КР или ДВ).
Возвращает:
- total_coords: общее количество координат ГЛ
- new_coords: количество новых координат ГЛ (уникальные имена, появившиеся впервые)
- transfer_delta: сумма дельт переносов по новым транспондерам
"""
# Базовый queryset для зоны
zone_qs = ObjItem.objects.filter(
geo_obj__isnull=False,
geo_obj__timestamp__isnull=False,
parameter_obj__id_satellite__location_place=location_place
)
if date_from:
zone_qs = zone_qs.filter(geo_obj__timestamp__date__gte=date_from)
if date_to:
zone_qs = zone_qs.filter(geo_obj__timestamp__date__lte=date_to)
# Общее количество координат ГЛ
total_coords = zone_qs.count()
# Новые координаты ГЛ (уникальные имена, появившиеся впервые в периоде)
new_coords = 0
if date_from:
# Имена, которые были ДО периода
existing_names = set(
ObjItem.objects.filter(
geo_obj__isnull=False,
geo_obj__timestamp__isnull=False,
geo_obj__timestamp__date__lt=date_from,
parameter_obj__id_satellite__location_place=location_place,
name__isnull=False
).exclude(name='').values_list('name', flat=True).distinct()
)
# Имена в периоде
period_names = set(
zone_qs.filter(name__isnull=False).exclude(name='').values_list('name', flat=True).distinct()
)
new_coords = len(period_names - existing_names)
# Расчёт дельты переносов по новым транспондерам
transfer_delta = self._calculate_transfer_delta(date_from, date_to, location_place)
return {
'total_coords': total_coords,
'new_coords': new_coords,
'transfer_delta': transfer_delta,
}
def _calculate_transfer_delta(self, date_from, date_to, location_place):
"""
Вычисляет сумму дельт по downlink для новых транспондеров.
Логика:
1. Берём все новые транспондеры за период (по created_at)
2. Для каждого ищем предыдущий транспондер с таким же именем, спутником и зоной
3. Вычисляем дельту по downlink
4. Суммируем все дельты
"""
if not date_from:
return 0.0
# Новые транспондеры за период для данной зоны
new_transponders_qs = Transponders.objects.filter(
sat_id__location_place=location_place,
created_at__date__gte=date_from
)
if date_to:
new_transponders_qs = new_transponders_qs.filter(created_at__date__lte=date_to)
total_delta = 0.0
for transponder in new_transponders_qs:
if not transponder.name or not transponder.sat_id or transponder.downlink is None:
continue
# Ищем предыдущий транспондер с таким же именем, спутником и зоной
previous = Transponders.objects.filter(
name=transponder.name,
sat_id=transponder.sat_id,
zone_name=transponder.zone_name,
created_at__lt=transponder.created_at,
downlink__isnull=False
).order_by('-created_at').first()
if previous and previous.downlink is not None:
delta = abs(transponder.downlink - previous.downlink)
total_delta += delta
return round(total_delta, 2)
def _get_kubsat_statistics(self, date_from, date_to):
"""
Получает статистику по Кубсатам из SourceRequest.
Возвращает:
- planned_count: количество запланированных сеансов
- conducted_count: количество проведённых
- canceled_gso_count: количество отменённых ГСО
- canceled_kub_count: количество отменённых МКА
"""
# Базовый queryset для заявок
requests_qs = SourceRequest.objects.all()
# Фильтруем по дате создания или planned_at
if date_from:
requests_qs = requests_qs.filter(
Q(created_at__date__gte=date_from) | Q(planned_at__date__gte=date_from)
)
if date_to:
requests_qs = requests_qs.filter(
Q(created_at__date__lte=date_to) | Q(planned_at__date__lte=date_to)
)
# Получаем ID заявок, у которых в истории был статус 'planned'
# Это заявки, которые были запланированы в выбранном периоде
history_qs = SourceRequestStatusHistory.objects.filter(
new_status='planned'
)
if date_from:
history_qs = history_qs.filter(changed_at__date__gte=date_from)
if date_to:
history_qs = history_qs.filter(changed_at__date__lte=date_to)
planned_request_ids = set(history_qs.values_list('source_request_id', flat=True))
# Также добавляем заявки, которые были созданы со статусом 'planned' в периоде
created_planned_qs = SourceRequest.objects.filter(status='planned')
if date_from:
created_planned_qs = created_planned_qs.filter(created_at__date__gte=date_from)
if date_to:
created_planned_qs = created_planned_qs.filter(created_at__date__lte=date_to)
planned_request_ids.update(created_planned_qs.values_list('id', flat=True))
planned_count = len(planned_request_ids)
# Считаем статусы из истории для запланированных заявок
conducted_count = 0
canceled_gso_count = 0
canceled_kub_count = 0
if planned_request_ids:
# Получаем историю статусов для запланированных заявок
status_history = SourceRequestStatusHistory.objects.filter(
source_request_id__in=planned_request_ids
)
if date_from:
status_history = status_history.filter(changed_at__date__gte=date_from)
if date_to:
status_history = status_history.filter(changed_at__date__lte=date_to)
# Считаем уникальные заявки по каждому статусу
conducted_ids = set(status_history.filter(new_status='conducted').values_list('source_request_id', flat=True))
canceled_gso_ids = set(status_history.filter(new_status='canceled_gso').values_list('source_request_id', flat=True))
canceled_kub_ids = set(status_history.filter(new_status='canceled_kub').values_list('source_request_id', flat=True))
conducted_count = len(conducted_ids)
canceled_gso_count = len(canceled_gso_ids)
canceled_kub_count = len(canceled_kub_ids)
return {
'planned_count': planned_count,
'conducted_count': conducted_count,
'canceled_gso_count': canceled_gso_count,
'canceled_kub_count': canceled_kub_count,
}
def get_extended_statistics(self, date_from, date_to):
"""Получает расширенную статистику по зонам и Кубсатам."""
kr_stats = self._get_zone_statistics(date_from, date_to, 'kr')
dv_stats = self._get_zone_statistics(date_from, date_to, 'dv')
kubsat_stats = self._get_kubsat_statistics(date_from, date_to)
return {
'kr': kr_stats,
'dv': dv_stats,
'kubsat': kubsat_stats,
}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -230,6 +416,9 @@ class StatisticsView(TemplateView):
# Получаем статистику
stats = self.get_statistics(date_from, date_to, satellite_ids, location_places)
# Получаем расширенную статистику
extended_stats = self.get_extended_statistics(date_from, date_to)
# Сериализуем данные для JavaScript
daily_data_json = json.dumps([
{
@@ -241,6 +430,7 @@ class StatisticsView(TemplateView):
])
satellite_stats_json = json.dumps(stats['satellite_stats'])
extended_stats_json = json.dumps(extended_stats)
context.update({
'satellites': satellites,
@@ -257,6 +447,8 @@ class StatisticsView(TemplateView):
'satellite_stats': stats['satellite_stats'],
'daily_data': daily_data_json,
'satellite_stats_json': satellite_stats_json,
'extended_stats': extended_stats,
'extended_stats_json': extended_stats_json,
})
return context
@@ -270,6 +462,7 @@ class StatisticsAPIView(StatisticsView):
satellite_ids = self.get_selected_satellites()
location_places = self.get_selected_location_places()
stats = self.get_statistics(date_from, date_to, satellite_ids, location_places)
extended_stats = self.get_extended_statistics(date_from, date_to)
# Преобразуем даты в строки для JSON
daily_data = []
@@ -287,4 +480,19 @@ class StatisticsAPIView(StatisticsView):
'new_emission_objects': stats['new_emission_objects'],
'satellite_stats': stats['satellite_stats'],
'daily_data': daily_data,
'extended_stats': extended_stats,
})
class ExtendedStatisticsAPIView(StatisticsView):
"""API endpoint для получения расширенной статистики в JSON формате."""
def get(self, request, *args, **kwargs):
date_from, date_to, preset = self.get_date_range()
extended_stats = self.get_extended_statistics(date_from, date_to)
return JsonResponse({
'extended_stats': extended_stats,
'date_from': date_from.isoformat() if date_from else None,
'date_to': date_to.isoformat() if date_to else None,
})