291 lines
12 KiB
Python
291 lines
12 KiB
Python
"""
|
||
Представление для страницы статистики.
|
||
"""
|
||
import json
|
||
from datetime import timedelta
|
||
from django.db.models import Count, Q, Min
|
||
from django.db.models.functions import TruncDate
|
||
from django.utils import timezone
|
||
from django.views.generic import TemplateView
|
||
from django.http import JsonResponse
|
||
|
||
from ..models import ObjItem, Source, Satellite, Geo
|
||
|
||
|
||
class StatisticsView(TemplateView):
|
||
"""Страница статистики по данным геолокации."""
|
||
|
||
template_name = 'mainapp/statistics.html'
|
||
|
||
def get_date_range(self):
|
||
"""Получает диапазон дат из параметров запроса."""
|
||
date_from = self.request.GET.get('date_from')
|
||
date_to = self.request.GET.get('date_to')
|
||
preset = self.request.GET.get('preset')
|
||
|
||
now = timezone.now()
|
||
|
||
# Обработка пресетов
|
||
if preset == 'week':
|
||
date_from = (now - timedelta(days=7)).date()
|
||
date_to = now.date()
|
||
elif preset == 'month':
|
||
date_from = (now - timedelta(days=30)).date()
|
||
date_to = now.date()
|
||
elif preset == '3months':
|
||
date_from = (now - timedelta(days=90)).date()
|
||
date_to = now.date()
|
||
elif preset == '6months':
|
||
date_from = (now - timedelta(days=180)).date()
|
||
date_to = now.date()
|
||
elif preset == 'all':
|
||
date_from = None
|
||
date_to = None
|
||
else:
|
||
# Парсинг дат из параметров
|
||
from datetime import datetime
|
||
if date_from:
|
||
try:
|
||
date_from = datetime.strptime(date_from, '%Y-%m-%d').date()
|
||
except ValueError:
|
||
date_from = None
|
||
if date_to:
|
||
try:
|
||
date_to = datetime.strptime(date_to, '%Y-%m-%d').date()
|
||
except ValueError:
|
||
date_to = None
|
||
|
||
return date_from, date_to, preset
|
||
|
||
def get_selected_satellites(self):
|
||
"""Получает выбранные спутники из параметров запроса."""
|
||
satellite_ids = self.request.GET.getlist('satellite_id')
|
||
return [int(sid) for sid in satellite_ids if sid.isdigit()]
|
||
|
||
def get_selected_location_places(self):
|
||
"""Получает выбранные комплексы из параметров запроса."""
|
||
return self.request.GET.getlist('location_place')
|
||
|
||
def get_base_queryset(self, date_from, date_to, satellite_ids, location_places=None):
|
||
"""Возвращает базовый queryset ObjItem с фильтрами."""
|
||
qs = ObjItem.objects.filter(
|
||
geo_obj__isnull=False,
|
||
geo_obj__timestamp__isnull=False
|
||
)
|
||
|
||
if date_from:
|
||
qs = qs.filter(geo_obj__timestamp__date__gte=date_from)
|
||
if date_to:
|
||
qs = qs.filter(geo_obj__timestamp__date__lte=date_to)
|
||
if satellite_ids:
|
||
qs = qs.filter(parameter_obj__id_satellite__id__in=satellite_ids)
|
||
if location_places:
|
||
qs = qs.filter(parameter_obj__id_satellite__location_place__in=location_places)
|
||
|
||
return qs
|
||
|
||
def get_statistics(self, date_from, date_to, satellite_ids, location_places=None):
|
||
"""Вычисляет основную статистику."""
|
||
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids, location_places)
|
||
|
||
# Общее количество точек
|
||
total_points = base_qs.count()
|
||
|
||
# Количество уникальных объектов (Source)
|
||
total_sources = base_qs.filter(source__isnull=False).values('source').distinct().count()
|
||
|
||
# Новые излучения - объекты, у которых имя появилось впервые в выбранном периоде
|
||
new_emissions_data = self._calculate_new_emissions(date_from, date_to, satellite_ids, location_places)
|
||
|
||
# Статистика по спутникам
|
||
satellite_stats = self._get_satellite_statistics(date_from, date_to, satellite_ids, location_places)
|
||
|
||
# Данные для графика по дням
|
||
daily_data = self._get_daily_statistics(date_from, date_to, satellite_ids, location_places)
|
||
|
||
return {
|
||
'total_points': total_points,
|
||
'total_sources': total_sources,
|
||
'new_emissions_count': new_emissions_data['count'],
|
||
'new_emission_objects': new_emissions_data['objects'],
|
||
'satellite_stats': satellite_stats,
|
||
'daily_data': daily_data,
|
||
}
|
||
|
||
def _calculate_new_emissions(self, date_from, date_to, satellite_ids, location_places=None):
|
||
"""
|
||
Вычисляет новые излучения - уникальные имена объектов,
|
||
которые появились впервые в выбранном периоде.
|
||
|
||
Возвращает количество уникальных новых имён и данные об объектах.
|
||
Оптимизировано для минимизации SQL запросов.
|
||
"""
|
||
if not date_from:
|
||
# Если нет начальной даты, берём все данные - новых излучений нет
|
||
return {'count': 0, 'objects': []}
|
||
|
||
# Получаем все имена объектов, которые появились ДО выбранного периода
|
||
existing_names = set(
|
||
ObjItem.objects.filter(
|
||
geo_obj__isnull=False,
|
||
geo_obj__timestamp__isnull=False,
|
||
geo_obj__timestamp__date__lt=date_from,
|
||
name__isnull=False
|
||
).exclude(name='').values_list('name', flat=True).distinct()
|
||
)
|
||
|
||
# Базовый queryset для выбранного периода
|
||
period_qs = self.get_base_queryset(date_from, date_to, satellite_ids, location_places).filter(
|
||
name__isnull=False
|
||
).exclude(name='')
|
||
|
||
# Получаем уникальные имена в выбранном периоде
|
||
period_names = set(period_qs.values_list('name', flat=True).distinct())
|
||
|
||
# Новые имена = имена в периоде, которых не было раньше
|
||
new_names = period_names - existing_names
|
||
|
||
if not new_names:
|
||
return {'count': 0, 'objects': []}
|
||
|
||
# Оптимизация: получаем все данные одним запросом с группировкой по имени
|
||
# Используем values() для получения уникальных комбинаций name + info + ownership
|
||
objitems_data = period_qs.filter(
|
||
name__in=new_names
|
||
).select_related(
|
||
'source__info', 'source__ownership'
|
||
).values(
|
||
'name',
|
||
'source__info__name',
|
||
'source__ownership__name'
|
||
).distinct()
|
||
|
||
# Собираем данные, оставляя только первую запись для каждого имени
|
||
seen_names = set()
|
||
new_objects = []
|
||
|
||
for item in objitems_data:
|
||
name = item['name']
|
||
if name not in seen_names:
|
||
seen_names.add(name)
|
||
new_objects.append({
|
||
'name': name,
|
||
'info': item['source__info__name'] or '-',
|
||
'ownership': item['source__ownership__name'] or '-',
|
||
})
|
||
|
||
# Сортируем по имени
|
||
new_objects.sort(key=lambda x: x['name'])
|
||
|
||
return {'count': len(new_names), 'objects': new_objects}
|
||
|
||
def _get_satellite_statistics(self, date_from, date_to, satellite_ids, location_places=None):
|
||
"""Получает статистику по каждому спутнику."""
|
||
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids, location_places)
|
||
|
||
# Группируем по спутникам
|
||
stats = base_qs.filter(
|
||
parameter_obj__id_satellite__isnull=False
|
||
).values(
|
||
'parameter_obj__id_satellite__id',
|
||
'parameter_obj__id_satellite__name'
|
||
).annotate(
|
||
points_count=Count('id'),
|
||
sources_count=Count('source', distinct=True)
|
||
).order_by('-points_count')
|
||
|
||
return list(stats)
|
||
|
||
def _get_daily_statistics(self, date_from, date_to, satellite_ids, location_places=None):
|
||
"""Получает статистику по дням для графика."""
|
||
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids, location_places)
|
||
|
||
daily = base_qs.annotate(
|
||
date=TruncDate('geo_obj__timestamp')
|
||
).values('date').annotate(
|
||
points=Count('id'),
|
||
sources=Count('source', distinct=True)
|
||
).order_by('date')
|
||
|
||
return list(daily)
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
|
||
date_from, date_to, preset = self.get_date_range()
|
||
satellite_ids = self.get_selected_satellites()
|
||
location_places = self.get_selected_location_places()
|
||
|
||
# Получаем только спутники, у которых есть точки ГЛ
|
||
satellites_with_points = ObjItem.objects.filter(
|
||
geo_obj__isnull=False,
|
||
geo_obj__timestamp__isnull=False,
|
||
parameter_obj__id_satellite__isnull=False
|
||
).values_list('parameter_obj__id_satellite__id', flat=True).distinct()
|
||
|
||
satellites = Satellite.objects.filter(
|
||
id__in=satellites_with_points
|
||
).order_by('name')
|
||
|
||
# Получаем статистику
|
||
stats = self.get_statistics(date_from, date_to, satellite_ids, location_places)
|
||
|
||
# Сериализуем данные для JavaScript
|
||
daily_data_json = json.dumps([
|
||
{
|
||
'date': item['date'].isoformat() if item['date'] else None,
|
||
'points': item['points'],
|
||
'sources': item['sources'],
|
||
}
|
||
for item in stats['daily_data']
|
||
])
|
||
|
||
satellite_stats_json = json.dumps(stats['satellite_stats'])
|
||
|
||
context.update({
|
||
'satellites': satellites,
|
||
'selected_satellites': satellite_ids,
|
||
'location_places': Satellite.PLACES,
|
||
'selected_location_places': location_places,
|
||
'date_from': date_from.isoformat() if date_from else '',
|
||
'date_to': date_to.isoformat() if date_to else '',
|
||
'preset': preset or '',
|
||
'total_points': stats['total_points'],
|
||
'total_sources': stats['total_sources'],
|
||
'new_emissions_count': stats['new_emissions_count'],
|
||
'new_emission_objects': stats['new_emission_objects'],
|
||
'satellite_stats': stats['satellite_stats'],
|
||
'daily_data': daily_data_json,
|
||
'satellite_stats_json': satellite_stats_json,
|
||
})
|
||
|
||
return context
|
||
|
||
|
||
class StatisticsAPIView(StatisticsView):
|
||
"""API endpoint для получения статистики в JSON формате."""
|
||
|
||
def get(self, request, *args, **kwargs):
|
||
date_from, date_to, preset = self.get_date_range()
|
||
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)
|
||
|
||
# Преобразуем даты в строки для JSON
|
||
daily_data = []
|
||
for item in stats['daily_data']:
|
||
daily_data.append({
|
||
'date': item['date'].isoformat() if item['date'] else None,
|
||
'points': item['points'],
|
||
'sources': item['sources'],
|
||
})
|
||
|
||
return JsonResponse({
|
||
'total_points': stats['total_points'],
|
||
'total_sources': stats['total_sources'],
|
||
'new_emissions_count': stats['new_emissions_count'],
|
||
'new_emission_objects': stats['new_emission_objects'],
|
||
'satellite_stats': stats['satellite_stats'],
|
||
'daily_data': daily_data,
|
||
})
|