Files
dbstorage/dbapp/mainapp/views/statistics.py

281 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Представление для страницы статистики.
"""
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_base_queryset(self, date_from, date_to, satellite_ids):
"""Возвращает базовый 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)
return qs
def get_statistics(self, date_from, date_to, satellite_ids):
"""Вычисляет основную статистику."""
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids)
# Общее количество точек
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)
# Статистика по спутникам
satellite_stats = self._get_satellite_statistics(date_from, date_to, satellite_ids)
# Данные для графика по дням
daily_data = self._get_daily_statistics(date_from, date_to, satellite_ids)
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):
"""
Вычисляет новые излучения - уникальные имена объектов,
которые появились впервые в выбранном периоде.
Возвращает количество уникальных новых имён и данные об объектах.
Оптимизировано для минимизации 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).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):
"""Получает статистику по каждому спутнику."""
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids)
# Группируем по спутникам
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):
"""Получает статистику по дням для графика."""
base_qs = self.get_base_queryset(date_from, date_to, satellite_ids)
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()
# Получаем только спутники, у которых есть точки ГЛ
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)
# Сериализуем данные для 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,
'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()
stats = self.get_statistics(date_from, date_to, satellite_ids)
# Преобразуем даты в строки для 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,
})