from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect from django.urls import reverse from django.views.generic import TemplateView, View from trends.models import TrendResult from trends.trends import TREND_REGISTRY from trends.utils import get_disabled_trends, get_period_nav, get_supported_periods TREND_METADATA = { "activity-distribution": { "title": "Activity Distribution", "description": "How your scrobbles are divided across media types.", "icon": "📊", }, "concurrent-listening": { "title": "Concurrent Listening", "description": "What music were you listening to while on trails or at locations?", "icon": "🎧", }, "concurrent-reading": { "title": "Concurrent Reading", "description": "What music did you listen to while reading books?", "icon": "📖", }, "mood-trajectory": { "title": "Mood Trajectory", "description": "How your mood quality has changed over time.", "icon": "📈", }, "mood-by-time": { "title": "Mood by Time", "description": "How your mood varies by hour of day and day of week.", "icon": "🕐", }, "mood-distribution": { "title": "Mood Distribution", "description": "Which moods you feel most often.", "icon": "🎭", }, "mood-streaks": { "title": "Mood Streaks", "description": "Your longest runs of positive and negative moods.", "icon": "🔥", }, "mood-weather": { "title": "Mood & Weather", "description": "How weather conditions correlate with your mood.", "icon": "🌤", }, "peak-hours": { "title": "Peak Activity Hours", "description": "What time of day are you most active?", "icon": "🕐", }, "reading-pace-vs-activity": { "title": "Reading Pace vs Music", "description": "Compare how long you read per session with and without concurrent music.", "icon": "📊", }, "time-of-day-categories": { "title": "Time of Day Categories", "description": "Are you an early bird, day jay, or night owl? Categorized by Books, Trails, Birding Locations, and Board Games.", "icon": "🦉", }, "trending-up": { "title": "Trending Media Types", "description": "Which media types have you been consuming more or less of recently?", "icon": "📈", }, "weekly-rhythm": { "title": "Weekly Rhythm", "description": "Which days of the week see the most scrobble activity?", "icon": "📅", }, } class TrendListView(LoginRequiredMixin, TemplateView): template_name = "trends/trend_list.html" def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) disabled = get_disabled_trends(self.request.user) results = TrendResult.objects.filter( user=self.request.user, ).order_by("trend_slug", "-computed_at") latest_by_slug = {} for r in results: if r.trend_slug not in latest_by_slug: latest_by_slug[r.trend_slug] = r trends = [] for slug in TREND_REGISTRY: if slug in disabled: continue meta = TREND_METADATA.get(slug, {}) result = latest_by_slug.get(slug) is_disabled = slug in (self.request.user.profile.disabled_trends or []) trends.append( { "slug": slug, "title": meta.get("title", slug), "description": meta.get("description", ""), "icon": meta.get("icon", ""), "computed_at": result.computed_at if result else None, "has_data": result is not None, "disabled": is_disabled, } ) ctx["trends"] = trends return ctx class TrendDetailView(LoginRequiredMixin, TemplateView): template_name = "trends/trend_detail.html" def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) slug = kwargs["trend_slug"] if slug not in TREND_REGISTRY: ctx["trend_not_found"] = True return ctx period = self.request.GET.get("period", "last_30") meta = TREND_METADATA.get(slug, {}) ctx["trend"] = { "slug": slug, "title": meta.get("title", slug), "description": meta.get("description", ""), "icon": meta.get("icon", ""), } supported = get_supported_periods(slug) ctx["supported_periods"] = supported ctx["current_period"] = period ctx["current_period_label"] = supported.get(period, "") prev_period, next_period = get_period_nav(period, slug) ctx["prev_period"] = prev_period ctx["next_period"] = next_period result = TrendResult.objects.filter( user=self.request.user, trend_slug=slug, period=period, ).first() if result: ctx["computed_at"] = result.computed_at ctx["data"] = result.data else: ctx["computed_at"] = None ctx["data"] = None disabled = get_disabled_trends(self.request.user) ctx["trend_disabled"] = slug in disabled ctx["globally_disabled"] = self.request.user.profile.trends_disabled return ctx class ToggleTrendDisabledView(LoginRequiredMixin, View): def post(self, request, trend_slug): profile = request.user.profile disabled = set(profile.disabled_trends or []) if trend_slug in disabled: disabled.discard(trend_slug) messages.success(request, f"Trend re-enabled.") else: disabled.add(trend_slug) messages.success(request, f"Trend disabled.") profile.disabled_trends = list(disabled) profile.save(update_fields=["disabled_trends"]) return HttpResponseRedirect( request.META.get("HTTP_REFERER", reverse("trends:trends-home")) )