Добавил локально библиотеку chart js. Сделал секретную статистику
This commit is contained in:
287
dbapp/mainapp/views/secret_stats.py
Normal file
287
dbapp/mainapp/views/secret_stats.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
Секретная страница статистики в стиле 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
|
||||
Reference in New Issue
Block a user