""" Секретная страница статистики в стиле Spotify Wrapped / Яндекс.Музыка. Красивые анимации, диаграммы и визуализации. """ import json from datetime import timedelta, datetime from collections import defaultdict from django.db.models import Count, Q, Min, Max, Avg, Sum from django.db.models.functions import TruncDate, TruncMonth, ExtractWeekDay, ExtractHour from django.utils import timezone from django.views.generic import TemplateView from ..models import ObjItem, Source, Satellite, Geo, Parameter class SecretStatsView(TemplateView): """Секретная страница статистики - итоги года в стиле Spotify Wrapped.""" template_name = 'mainapp/secret_stats.html' def get_year_range(self): """Получает диапазон дат для текущего года.""" now = timezone.now() year = self.request.GET.get('year', now.year) try: year = int(year) except (ValueError, TypeError): year = now.year date_from = datetime(year, 1, 1).date() date_to = datetime(year, 12, 31).date() return date_from, date_to, year def get_base_queryset(self, date_from, date_to): """Возвращает базовый queryset ObjItem с фильтрами по дате ГЛ.""" qs = ObjItem.objects.filter( geo_obj__isnull=False, geo_obj__timestamp__isnull=False, geo_obj__timestamp__date__gte=date_from, geo_obj__timestamp__date__lte=date_to ) return qs def get_main_stats(self, date_from, date_to): """Основная статистика: точки и объекты.""" base_qs = self.get_base_queryset(date_from, date_to) total_points = base_qs.count() total_sources = base_qs.filter(source__isnull=False).values('source').distinct().count() return { 'total_points': total_points, 'total_sources': total_sources, } def get_new_emissions(self, date_from, date_to): """ Новые излучения - объекты, у которых имя появилось впервые в выбранном периоде. """ # Получаем все имена объектов, которые появились ДО выбранного периода 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).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': [], 'sources_count': 0} # Получаем данные о новых объектах 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']) # Количество источников для новых излучений new_sources_count = period_qs.filter( name__in=new_names, source__isnull=False ).values('source').distinct().count() return { 'count': len(new_names), 'objects': new_objects[:20], # Топ-20 для отображения 'sources_count': new_sources_count } def get_satellite_stats(self, date_from, date_to): """Статистика по спутникам.""" base_qs = self.get_base_queryset(date_from, date_to) 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), unique_names=Count('name', distinct=True) ).order_by('-points_count') return list(stats) def get_monthly_stats(self, date_from, date_to): """Статистика по месяцам.""" base_qs = self.get_base_queryset(date_from, date_to) monthly = base_qs.annotate( month=TruncMonth('geo_obj__timestamp') ).values('month').annotate( points=Count('id'), sources=Count('source', distinct=True) ).order_by('month') return list(monthly) def get_weekday_stats(self, date_from, date_to): """Статистика по дням недели.""" base_qs = self.get_base_queryset(date_from, date_to) weekday = base_qs.annotate( weekday=ExtractWeekDay('geo_obj__timestamp') ).values('weekday').annotate( points=Count('id') ).order_by('weekday') return list(weekday) def get_hourly_stats(self, date_from, date_to): """Статистика по часам.""" base_qs = self.get_base_queryset(date_from, date_to) hourly = base_qs.annotate( hour=ExtractHour('geo_obj__timestamp') ).values('hour').annotate( points=Count('id') ).order_by('hour') return list(hourly) def get_top_objects(self, date_from, date_to): """Топ объектов по количеству точек.""" base_qs = self.get_base_queryset(date_from, date_to) top = base_qs.filter( name__isnull=False ).exclude(name='').values('name').annotate( points=Count('id') ).order_by('-points')[:10] return list(top) def get_busiest_day(self, date_from, date_to): """Самый активный день.""" base_qs = self.get_base_queryset(date_from, date_to) daily = base_qs.annotate( date=TruncDate('geo_obj__timestamp') ).values('date').annotate( points=Count('id') ).order_by('-points').first() return daily def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) date_from, date_to, year = self.get_year_range() # Основная статистика main_stats = self.get_main_stats(date_from, date_to) # Новые излучения new_emissions = self.get_new_emissions(date_from, date_to) # Статистика по спутникам satellite_stats = self.get_satellite_stats(date_from, date_to) # Статистика по месяцам monthly_stats = self.get_monthly_stats(date_from, date_to) # Статистика по дням недели weekday_stats = self.get_weekday_stats(date_from, date_to) # Статистика по часам hourly_stats = self.get_hourly_stats(date_from, date_to) # Топ объектов top_objects = self.get_top_objects(date_from, date_to) # Самый активный день busiest_day = self.get_busiest_day(date_from, date_to) # Доступные годы для выбора years_with_data = ObjItem.objects.filter( geo_obj__isnull=False, geo_obj__timestamp__isnull=False ).dates('geo_obj__timestamp', 'year') available_years = sorted([d.year for d in years_with_data], reverse=True) # JSON данные для графиков monthly_data_json = json.dumps([ { 'month': item['month'].strftime('%Y-%m') if item['month'] else None, 'month_name': item['month'].strftime('%B') if item['month'] else None, 'points': item['points'], 'sources': item['sources'], } for item in monthly_stats ]) satellite_stats_json = json.dumps(satellite_stats) weekday_names = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'] weekday_data_json = json.dumps([ { 'weekday': item['weekday'], 'weekday_name': weekday_names[item['weekday'] - 1] if item['weekday'] else '', 'points': item['points'], } for item in weekday_stats ]) hourly_data_json = json.dumps([ { 'hour': item['hour'], 'points': item['points'], } for item in hourly_stats ]) top_objects_json = json.dumps(top_objects) context.update({ 'year': year, 'available_years': available_years, 'total_points': main_stats['total_points'], 'total_sources': main_stats['total_sources'], 'new_emissions_count': new_emissions['count'], 'new_emissions_sources': new_emissions['sources_count'], 'new_emission_objects': new_emissions['objects'], 'satellite_stats': satellite_stats[:10], # Топ-10 'satellite_count': len(satellite_stats), 'busiest_day': busiest_day, 'monthly_data_json': monthly_data_json, 'satellite_stats_json': satellite_stats_json, 'weekday_data_json': weekday_data_json, 'hourly_data_json': hourly_data_json, 'top_objects_json': top_objects_json, }) return context