""" Представление для страницы статистики. """ 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, })