773 lines
25 KiB
Python
773 lines
25 KiB
Python
import calendar
|
|
from datetime import timedelta
|
|
|
|
from charts.models import ChartRecord
|
|
from django.db.models import Count, Q
|
|
from django.http import Http404
|
|
from django.utils import timezone
|
|
from django.views.generic import TemplateView
|
|
from profiles.utils import now_user_timezone
|
|
from scrobbles.models import Scrobble
|
|
|
|
MEDIA_TYPE_FILTERS = {
|
|
"artist": Q(artist__isnull=False),
|
|
"album": Q(album__isnull=False),
|
|
"track": Q(track__isnull=False),
|
|
"tv_series": Q(tv_series__isnull=False),
|
|
"video": Q(video__isnull=False),
|
|
"podcast": Q(podcast__isnull=False),
|
|
"podcast_episode": Q(podcast_episode__isnull=False),
|
|
"board_game": Q(board_game__isnull=False),
|
|
"trail": Q(trail__isnull=False),
|
|
"geo_location": Q(geo_location__isnull=False),
|
|
"food": Q(food__isnull=False),
|
|
"book": Q(book__isnull=False),
|
|
}
|
|
|
|
|
|
class ChartRecordView(TemplateView):
|
|
template_name = "charts/chart_index.html"
|
|
|
|
def get_charts(
|
|
self,
|
|
user,
|
|
media_type: str,
|
|
year=None,
|
|
month=None,
|
|
week=None,
|
|
day=None,
|
|
limit=20,
|
|
):
|
|
params = {"user": user}
|
|
if year is not None:
|
|
params["year"] = year
|
|
if month is not None:
|
|
params["month"] = month
|
|
if week is not None:
|
|
params["week"] = week
|
|
if day is not None:
|
|
params["day"] = day
|
|
|
|
filter_key = media_type.lower()
|
|
media_filter = MEDIA_TYPE_FILTERS.get(filter_key, Q())
|
|
|
|
return ChartRecord.objects.filter(media_filter, **params).order_by("rank")[
|
|
:limit
|
|
]
|
|
|
|
def get_charts_for_period(
|
|
self,
|
|
user,
|
|
media_type: str,
|
|
year=None,
|
|
month=None,
|
|
week=None,
|
|
day=None,
|
|
limit=20,
|
|
):
|
|
"""Get charts for a specific period, properly filtering by period type."""
|
|
params = {"user": user}
|
|
if year is not None:
|
|
params["year"] = year
|
|
if month is not None:
|
|
params["month"] = month
|
|
if week is not None:
|
|
params["week"] = week
|
|
if day is not None:
|
|
params["day"] = day
|
|
|
|
filter_key = media_type.lower()
|
|
media_filter = MEDIA_TYPE_FILTERS.get(filter_key, Q())
|
|
|
|
qs = ChartRecord.objects.filter(media_filter, **params)
|
|
|
|
if day is not None:
|
|
qs = qs.filter(day__isnull=False)
|
|
elif week is not None:
|
|
qs = qs.filter(week__isnull=False, day__isnull=True)
|
|
elif month is not None:
|
|
qs = qs.filter(month__isnull=False, week__isnull=True, day__isnull=True)
|
|
else:
|
|
qs = qs.filter(month__isnull=True, week__isnull=True, day__isnull=True)
|
|
|
|
return qs.order_by("rank")[:limit]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
chart_type = self.request.GET.get("chart_type", "standard")
|
|
context["chart_type"] = chart_type
|
|
|
|
date_param = self.request.GET.get("date")
|
|
|
|
now = timezone.now()
|
|
if user.is_authenticated:
|
|
now = now_user_timezone(user.profile)
|
|
today = now.date()
|
|
current_year = today.year
|
|
current_month = today.month
|
|
current_week = today.isocalendar()[1]
|
|
current_day = today.day
|
|
|
|
context["current_year"] = current_year
|
|
context["current_month"] = current_month
|
|
context["current_week"] = current_week
|
|
context["current_day"] = current_day
|
|
|
|
# Resolve date parameters
|
|
if date_param:
|
|
parts = date_param.split("-")
|
|
year = int(parts[0])
|
|
week = None
|
|
month = None
|
|
day = None
|
|
if len(parts) >= 2 and parts[1].startswith("W"):
|
|
week = int(parts[1].lstrip("W"))
|
|
elif len(parts) >= 2 and parts[1]:
|
|
try:
|
|
month = int(parts[1])
|
|
except ValueError:
|
|
pass
|
|
if len(parts) >= 3:
|
|
if parts[2].startswith("W"):
|
|
week = int(parts[2].lstrip("W"))
|
|
elif not parts[2].startswith("W"):
|
|
day = int(parts[2])
|
|
context["period"] = "historical"
|
|
context["year"] = year
|
|
context["month"] = month
|
|
context["month_name"] = calendar.month_name[month] if month else None
|
|
context["week"] = week
|
|
context["day"] = day
|
|
period_str = str(year)
|
|
if month:
|
|
period_str = f"{calendar.month_name[month]} {period_str}"
|
|
if week:
|
|
period_str = f"Week {week}, {period_str}"
|
|
if day:
|
|
period_str = f"{calendar.month_name[month]} {day}, {year}"
|
|
context["period_str"] = period_str
|
|
else:
|
|
year = current_year
|
|
month = current_month
|
|
week = current_week
|
|
day = current_day
|
|
context["period"] = "current"
|
|
context["year"] = current_year
|
|
context["month"] = current_month
|
|
context["month_name"] = calendar.month_name[current_month]
|
|
context["week"] = current_week
|
|
context["day"] = current_day
|
|
|
|
# List-group tables default to week-level when no date param (matches active tab)
|
|
if not date_param:
|
|
list_year = current_year
|
|
list_month = None
|
|
list_week = current_week
|
|
list_day = None
|
|
else:
|
|
list_year = year
|
|
list_month = month
|
|
list_week = week
|
|
list_day = day
|
|
|
|
context["charts"] = {
|
|
"artist": list(
|
|
self.get_charts_for_period(
|
|
user, "artist", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"album": list(
|
|
self.get_charts_for_period(
|
|
user, "album", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"track": list(
|
|
self.get_charts_for_period(
|
|
user, "track", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"tv_series": list(
|
|
self.get_charts_for_period(
|
|
user, "tv_series", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"video": list(
|
|
self.get_charts_for_period(
|
|
user, "video", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"board_game": list(
|
|
self.get_charts_for_period(
|
|
user, "board_game", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"book": list(
|
|
self.get_charts_for_period(
|
|
user, "book", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"food": list(
|
|
self.get_charts_for_period(
|
|
user, "food", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"podcast": list(
|
|
self.get_charts_for_period(
|
|
user, "podcast", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
"trail": list(
|
|
self.get_charts_for_period(
|
|
user, "trail", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
|
|
)
|
|
),
|
|
}
|
|
|
|
bird_data = self.get_bird_chart_data(
|
|
user,
|
|
year=context.get("year"),
|
|
month=context.get("month"),
|
|
week=context.get("week"),
|
|
day=context.get("day"),
|
|
)
|
|
context["birds_chart"] = bird_data["top_birds"]
|
|
context["bird_stats"] = {
|
|
"total_species": bird_data["total_species"],
|
|
"total_outings": bird_data["total_outings"],
|
|
"total_individuals": bird_data["total_individuals"],
|
|
}
|
|
|
|
context["chart_years"] = self.get_available_years(user)
|
|
context["period_type"] = self.get_period_type()
|
|
context["prev_period"] = self.get_prev_period_url(user)
|
|
context["next_period"] = self.get_next_period_url(user)
|
|
|
|
return context
|
|
|
|
def get_available_years(self, user):
|
|
if not hasattr(self, "_available_years"):
|
|
self._available_years = list(
|
|
ChartRecord.objects.filter(user=user)
|
|
.values_list("year", flat=True)
|
|
.distinct()
|
|
.order_by("-year")
|
|
)
|
|
return self._available_years
|
|
|
|
def get_period_type(self):
|
|
date_param = self.request.GET.get("date")
|
|
if not date_param:
|
|
return "current"
|
|
parts = date_param.split("-")
|
|
if len(parts) >= 3 and not parts[2].startswith("W"):
|
|
return "day"
|
|
if len(parts) >= 2 and parts[1].startswith("W"):
|
|
return "week"
|
|
if len(parts) >= 2:
|
|
return "month"
|
|
return "year"
|
|
|
|
def parse_date_params(self):
|
|
date_param = self.request.GET.get("date")
|
|
if not date_param:
|
|
return None, None, None, None
|
|
|
|
parts = date_param.split("-")
|
|
year = int(parts[0])
|
|
week = None
|
|
month = None
|
|
day = None
|
|
|
|
if len(parts) >= 2 and parts[1].startswith("W"):
|
|
week = int(parts[1].lstrip("W"))
|
|
elif len(parts) >= 2:
|
|
try:
|
|
month = int(parts[1])
|
|
except ValueError:
|
|
pass
|
|
|
|
if len(parts) >= 3:
|
|
if parts[2].startswith("W"):
|
|
week = int(parts[2].lstrip("W"))
|
|
else:
|
|
day = int(parts[2])
|
|
|
|
return year, month, week, day
|
|
|
|
def get_prev_period_url(self, user):
|
|
period_type = self.get_period_type()
|
|
|
|
if period_type == "current":
|
|
return None
|
|
|
|
year, month, week, day = self.parse_date_params()
|
|
if year is None:
|
|
return None
|
|
|
|
available_years = self.get_available_years(user)
|
|
|
|
if period_type == "day" and month and day:
|
|
if month == 1:
|
|
prev_month = 12
|
|
prev_year = year - 1
|
|
else:
|
|
prev_month = month - 1
|
|
prev_year = year
|
|
return f"/charts/?date={prev_year}-{prev_month:02d}"
|
|
elif period_type == "month" and month:
|
|
if month == 1:
|
|
prev_month = 12
|
|
prev_year = year - 1
|
|
else:
|
|
prev_month = month - 1
|
|
prev_year = year
|
|
return f"/charts/?date={prev_year}-{prev_month:02d}"
|
|
elif period_type == "week" and week:
|
|
if week == 1:
|
|
prev_week = 52
|
|
prev_year = year - 1
|
|
else:
|
|
prev_week = week - 1
|
|
prev_year = year
|
|
return f"/charts/?date={prev_year}-W{prev_week:02d}"
|
|
elif period_type == "year":
|
|
if year in available_years:
|
|
idx = available_years.index(year)
|
|
if idx + 1 < len(available_years):
|
|
return f"/charts/?date={available_years[idx + 1]}"
|
|
return f"/charts/?date={year - 1}"
|
|
return None
|
|
|
|
def get_next_period_url(self, user):
|
|
period_type = self.get_period_type()
|
|
|
|
if period_type == "current":
|
|
return None
|
|
|
|
year, month, week, day = self.parse_date_params()
|
|
if year is None:
|
|
return None
|
|
|
|
available_years = self.get_available_years(user)
|
|
|
|
if period_type == "day" and month and day:
|
|
if month == 12:
|
|
next_month = 1
|
|
next_year = year + 1
|
|
else:
|
|
next_month = month + 1
|
|
next_year = year
|
|
return f"/charts/?date={next_year}-{next_month:02d}"
|
|
elif period_type == "month" and month:
|
|
if month == 12:
|
|
next_month = 1
|
|
next_year = year + 1
|
|
else:
|
|
next_month = month + 1
|
|
next_year = year
|
|
return f"/charts/?date={next_year}-{next_month:02d}"
|
|
elif period_type == "week" and week:
|
|
if week == 52:
|
|
next_week = 1
|
|
next_year = year + 1
|
|
else:
|
|
next_week = week + 1
|
|
next_year = year
|
|
return f"/charts/?date={next_year}-W{next_week:02d}"
|
|
elif period_type == "year":
|
|
if year in available_years:
|
|
idx = available_years.index(year)
|
|
if idx > 0:
|
|
return f"/charts/?date={available_years[idx - 1]}"
|
|
return f"/charts/?date={year + 1}"
|
|
return None
|
|
|
|
def get_bird_chart_data(
|
|
self, user, year=None, month=None, week=None, day=None, limit=20
|
|
):
|
|
from birds.models import Bird
|
|
|
|
filters = {
|
|
"user": user,
|
|
"media_type": Scrobble.MediaType.BIRDING_LOCATION,
|
|
}
|
|
if year:
|
|
filters["timestamp__year"] = year
|
|
if month:
|
|
filters["timestamp__month"] = month
|
|
if week:
|
|
filters["timestamp__week"] = week
|
|
if day:
|
|
filters["timestamp__day"] = day
|
|
|
|
scrobbles = Scrobble.objects.filter(**filters)
|
|
|
|
bird_counts = {}
|
|
outings = 0
|
|
for scrobble in scrobbles.iterator():
|
|
outings += 1
|
|
birds_data = scrobble.log.get("birds", []) if scrobble.log else []
|
|
for entry in birds_data:
|
|
bird_id = entry.get("bird_id")
|
|
quantity = entry.get("quantity", 1)
|
|
if bird_id:
|
|
bird_counts[bird_id] = bird_counts.get(bird_id, 0) + quantity
|
|
|
|
sorted_birds = sorted(bird_counts.items(), key=lambda x: x[1], reverse=True)[
|
|
:limit
|
|
]
|
|
|
|
bird_ids = [bid for bid, _ in sorted_birds]
|
|
birds = Bird.objects.filter(id__in=bird_ids)
|
|
bird_map = {b.id: b for b in birds}
|
|
|
|
top_birds = [
|
|
{"bird": bird_map[bid], "count": count}
|
|
for bid, count in sorted_birds
|
|
if bid in bird_map
|
|
]
|
|
|
|
return {
|
|
"top_birds": top_birds,
|
|
"total_outings": outings,
|
|
"total_species": len(bird_counts),
|
|
"total_individuals": sum(bird_counts.values()),
|
|
}
|
|
|
|
|
|
class MalojaChartsView(ChartRecordView):
|
|
"""Three maloja-themed image grid widgets (artists, albums, TV series)
|
|
with Today/Week/Month/Year/All tabs. Each tab computes its own period
|
|
from the current date — no query param needed."""
|
|
|
|
template_name = "charts/maloja_charts.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ChartRecordView, self).get_context_data(**kwargs)
|
|
user = self.request.user
|
|
|
|
if not user.is_authenticated:
|
|
context["maloja_charts"] = {}
|
|
context["chart_keys"] = {}
|
|
return context
|
|
|
|
now = timezone.now()
|
|
now = now_user_timezone(user.profile)
|
|
today = now.date()
|
|
|
|
context["chart_keys"] = {
|
|
"today": "Today",
|
|
"week": "This Week",
|
|
"month": "This Month",
|
|
"year": "This Year",
|
|
"all": "All Time",
|
|
}
|
|
|
|
tab_params = {
|
|
"today": {"year": today.year, "month": today.month, "day": today.day},
|
|
"week": {"year": today.year, "week": today.isocalendar()[1]},
|
|
"month": {"year": today.year, "month": today.month},
|
|
"year": {"year": today.year},
|
|
}
|
|
|
|
maloja_charts = {}
|
|
for media_type in ("artist", "album", "tv_series"):
|
|
tabs = {}
|
|
for key in ("today", "week", "month", "year"):
|
|
tabs[key] = list(
|
|
self.get_charts_for_period(user, media_type, **tab_params[key])
|
|
)
|
|
tabs["all"] = list(
|
|
self.get_charts_for_period(user, media_type)
|
|
)
|
|
maloja_charts[media_type] = tabs
|
|
|
|
context["maloja_charts"] = maloja_charts
|
|
return context
|
|
|
|
|
|
MEDIA_TYPE_LABELS = {
|
|
"artist": ("🎤", "Top Artists"),
|
|
"album": ("💿", "Top Albums"),
|
|
"track": ("🎵", "Top Tracks"),
|
|
"tv_series": ("📺", "Top TV Series"),
|
|
"video": ("🎬", "Top Videos"),
|
|
"podcast": ("🎙️", "Top Podcasts"),
|
|
"podcast_episode": ("🎙️", "Top Podcast Episodes"),
|
|
"board_game": ("🎲", "Top Board Games"),
|
|
"book": ("📚", "Top Books"),
|
|
"food": ("🍽️", "Top Foods"),
|
|
"trail": ("🥾", "Top Trails"),
|
|
"geo_location": ("📍", "Top Locations"),
|
|
}
|
|
|
|
|
|
class ChartDetailView(TemplateView):
|
|
template_name = "charts/chart_detail.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
media_type = kwargs.get("media_type")
|
|
|
|
if media_type not in MEDIA_TYPE_FILTERS:
|
|
raise Http404
|
|
|
|
date_param = self.request.GET.get("date")
|
|
now = timezone.now()
|
|
if user.is_authenticated:
|
|
now = now_user_timezone(user.profile)
|
|
today = now.date()
|
|
|
|
year = today.year
|
|
month = None
|
|
week = None
|
|
day = None
|
|
|
|
if date_param:
|
|
parts = date_param.split("-")
|
|
year = int(parts[0])
|
|
if len(parts) >= 2 and parts[1].startswith("W"):
|
|
week = int(parts[1].lstrip("W"))
|
|
month = None
|
|
elif len(parts) >= 2:
|
|
try:
|
|
month = int(parts[1])
|
|
except ValueError:
|
|
pass
|
|
if len(parts) >= 3:
|
|
if parts[2].startswith("W"):
|
|
week = int(parts[2].lstrip("W"))
|
|
else:
|
|
day = int(parts[2])
|
|
|
|
params = {"user": user}
|
|
if year:
|
|
params["year"] = year
|
|
if month:
|
|
params["month"] = month
|
|
if week:
|
|
params["week"] = week
|
|
if day:
|
|
params["day"] = day
|
|
|
|
media_filter = MEDIA_TYPE_FILTERS.get(media_type, Q())
|
|
qs = ChartRecord.objects.filter(media_filter, **params)
|
|
|
|
if day is not None:
|
|
qs = qs.filter(day__isnull=False)
|
|
elif week is not None:
|
|
qs = qs.filter(week__isnull=False, day__isnull=True)
|
|
elif month is not None:
|
|
qs = qs.filter(month__isnull=False, week__isnull=True, day__isnull=True)
|
|
else:
|
|
qs = qs.filter(month__isnull=True, week__isnull=True, day__isnull=True)
|
|
|
|
context["page_charts"] = qs.order_by("rank")
|
|
context["media_type"] = media_type
|
|
emoji, label = MEDIA_TYPE_LABELS.get(media_type, ("", media_type))
|
|
context["media_label"] = label
|
|
context["media_emoji"] = emoji
|
|
context["year"] = year
|
|
context["month"] = month
|
|
context["week"] = week
|
|
context["day"] = day
|
|
|
|
context["chart_years"] = list(
|
|
ChartRecord.objects.filter(Q(user=user) & media_filter)
|
|
.values_list("year", flat=True)
|
|
.distinct()
|
|
.order_by("-year")
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class SpotifyTracksView(TemplateView):
|
|
template_name = "charts/spotify_tracks.html"
|
|
|
|
def get_spotify_tracks(self, user, limit=50):
|
|
non_spotify_mopidy_tracks = (
|
|
Scrobble.objects.filter(
|
|
user=user,
|
|
source="Mopidy",
|
|
track__isnull=False,
|
|
)
|
|
.exclude(log__mopidy_source="spotify")
|
|
.values("track")
|
|
)
|
|
track_ids = (
|
|
Scrobble.objects.filter(
|
|
user=user,
|
|
track__isnull=False,
|
|
)
|
|
.filter(Q(source="Last.fm") | Q(log__mopidy_source="spotify"))
|
|
.exclude(track__in=non_spotify_mopidy_tracks)
|
|
.values("track")
|
|
.annotate(count=Count("id"))
|
|
.order_by("-count")[:limit]
|
|
)
|
|
from music.models import Track
|
|
|
|
track_id_list = [item["track"] for item in track_ids]
|
|
tracks = Track.objects.filter(id__in=track_id_list)
|
|
track_map = {t.id: t for t in tracks}
|
|
return [
|
|
{
|
|
"track": track_map[tid],
|
|
"count": next(
|
|
(item["count"] for item in track_ids if item["track"] == tid), 0
|
|
),
|
|
}
|
|
for tid in track_id_list
|
|
if tid in track_map
|
|
]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
context["spotify_tracks"] = self.get_spotify_tracks(user)
|
|
return context
|
|
|
|
|
|
class BandcampTracksView(TemplateView):
|
|
template_name = "charts/bandcamp_tracks.html"
|
|
|
|
def get_bandcamp_tracks(self, user, limit=50):
|
|
non_bandcamp_mopidy_tracks = (
|
|
Scrobble.objects.filter(
|
|
user=user,
|
|
source="Mopidy",
|
|
track__isnull=False,
|
|
)
|
|
.exclude(log__mopidy_source="bandcamp")
|
|
.values("track")
|
|
)
|
|
track_ids = (
|
|
Scrobble.objects.filter(
|
|
user=user,
|
|
track__isnull=False,
|
|
log__mopidy_source="bandcamp",
|
|
)
|
|
.exclude(track__in=non_bandcamp_mopidy_tracks)
|
|
.values("track")
|
|
.annotate(count=Count("id"))
|
|
.order_by("-count")[:limit]
|
|
)
|
|
from music.models import Track
|
|
|
|
track_id_list = [item["track"] for item in track_ids]
|
|
tracks = Track.objects.filter(id__in=track_id_list)
|
|
track_map = {t.id: t for t in tracks}
|
|
return [
|
|
{
|
|
"track": track_map[tid],
|
|
"count": next(
|
|
(item["count"] for item in track_ids if item["track"] == tid), 0
|
|
),
|
|
}
|
|
for tid in track_id_list
|
|
if tid in track_map
|
|
]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
context["bandcamp_tracks"] = self.get_bandcamp_tracks(user)
|
|
return context
|
|
|
|
|
|
class BirdsChartView(TemplateView):
|
|
template_name = "charts/birds_chart.html"
|
|
|
|
def get_bird_data(self, user, year=None, month=None, week=None, day=None, limit=50):
|
|
from birds.models import Bird
|
|
|
|
filters = {
|
|
"user": user,
|
|
"media_type": Scrobble.MediaType.BIRDING_LOCATION,
|
|
}
|
|
if year:
|
|
filters["timestamp__year"] = year
|
|
if month:
|
|
filters["timestamp__month"] = month
|
|
if week:
|
|
filters["timestamp__week"] = week
|
|
if day:
|
|
filters["timestamp__day"] = day
|
|
|
|
scrobbles = Scrobble.objects.filter(**filters)
|
|
|
|
bird_counts = {}
|
|
outings = 0
|
|
for scrobble in scrobbles.iterator():
|
|
outings += 1
|
|
birds_data = scrobble.log.get("birds", []) if scrobble.log else []
|
|
for entry in birds_data:
|
|
bird_id = entry.get("bird_id")
|
|
quantity = entry.get("quantity", 1)
|
|
if bird_id:
|
|
bird_counts[bird_id] = bird_counts.get(bird_id, 0) + quantity
|
|
|
|
sorted_birds = sorted(bird_counts.items(), key=lambda x: x[1], reverse=True)[
|
|
:limit
|
|
]
|
|
|
|
bird_ids = [bid for bid, _ in sorted_birds]
|
|
birds = Bird.objects.filter(id__in=bird_ids)
|
|
bird_map = {b.id: b for b in birds}
|
|
|
|
top_birds = [
|
|
{"bird": bird_map[bid], "count": count}
|
|
for bid, count in sorted_birds
|
|
if bid in bird_map
|
|
]
|
|
|
|
return {
|
|
"top_birds": top_birds,
|
|
"total_outings": outings,
|
|
"total_species": len(bird_counts),
|
|
"total_individuals": sum(bird_counts.values()),
|
|
}
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
user = self.request.user
|
|
|
|
date_param = self.request.GET.get("date")
|
|
now = timezone.now()
|
|
if user.is_authenticated:
|
|
now = now_user_timezone(user.profile)
|
|
today = now.date()
|
|
|
|
year = None
|
|
month = None
|
|
week = None
|
|
day = None
|
|
|
|
if date_param:
|
|
parts = date_param.split("-")
|
|
year = int(parts[0])
|
|
if len(parts) >= 2 and parts[1].startswith("W"):
|
|
week = int(parts[1].lstrip("W"))
|
|
elif len(parts) >= 2:
|
|
try:
|
|
month = int(parts[1])
|
|
except ValueError:
|
|
pass
|
|
if len(parts) >= 3:
|
|
if parts[2].startswith("W"):
|
|
week = int(parts[2].lstrip("W"))
|
|
else:
|
|
day = int(parts[2])
|
|
|
|
bird_data = self.get_bird_data(user, year=year, month=month, week=week, day=day)
|
|
context["birds_chart"] = bird_data["top_birds"]
|
|
context["bird_stats"] = {
|
|
"total_species": bird_data["total_species"],
|
|
"total_outings": bird_data["total_outings"],
|
|
"total_individuals": bird_data["total_individuals"],
|
|
}
|
|
return context
|