This commit has no recorded session.
diff
commit 41a68291a4f10cf842396c64654247fba6bf924cAuthor: Colin Powell <colin@unbl.ink>Date: Thu Jun 25 18:58:23 2026 -0400 [trends] Allow disabling one or many or all trends@@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [1/24] :vrobbler:project:personal:+* Backlog [2/25] :vrobbler:project:personal: ** TODO [#C] After transition to linux add curl_cffi as webpage scrapper again :webpages:metadata: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles: :PROPERTIES:@@ -605,6 +605,10 @@ independent of the email flow it was originally creatdd for ** TODO [#B] Is there way to create unique slugs for media instances :media_types: +** DONE [#B] Allow people all trends or individual trends :trends:profiles:+:PROPERTIES:+:ID: 1d081152-abd1-73c2-a625-903565a10c6c+:END: ** DONE [#A] Fix a bug in board game scorelog data :boardgames:logdata: :PROPERTIES: :ID: 014bab30-13bf-fae7-e678-4666a8d38ae4@@ -42,6 +42,7 @@ class UserProfileForm(forms.ModelForm): "home_scrobble_limit", "live_now_playing", "weigh_in_units",+ "trends_disabled", ] widgets = { "lastfm_password": forms.PasswordInput(render_value=True),@@ -0,0 +1,27 @@+# Generated by Django 4.2.29 on 2026-06-25 20:43++from django.db import migrations, models+++class Migration(migrations.Migration):++ dependencies = [+ ("profiles", "0039_userprofile_live_now_playing"),+ ]++ operations = [+ migrations.AddField(+ model_name="userprofile",+ name="disabled_trends",+ field=models.JSONField(+ blank=True,+ default=list,+ help_text="List of trend slugs the user has disabled",+ ),+ ),+ migrations.AddField(+ model_name="userprofile",+ name="trends_disabled",+ field=models.BooleanField(default=False),+ ),+ ]@@ -106,6 +106,14 @@ class UserProfile(TimeStampedModel): default=WeighUnit.METRIC, ) + trends_disabled = models.BooleanField(default=False)++ disabled_trends = models.JSONField(+ default=list,+ blank=True,+ help_text="List of trend slugs the user has disabled",+ )+ def __str__(self): return f"User profile for {self.user}" @@ -38,15 +38,36 @@ class Command(BaseCommand): overall_start = timezone.now() ok_count = 0 fail_count = 0+ skipped_count = 0 for user in users:- total_trends = len(TREND_REGISTRY)- self.stdout.write(f" {user} ({user.id}): {total_trends} trends...")+ try:+ profile = user.profile+ if profile.trends_disabled:+ self.stdout.write(+ self.style.WARNING(+ f" {user} ({user.id}): trends disabled globally, skipping"+ )+ )+ skipped_count += len(TREND_REGISTRY)+ continue+ disabled_trends = set(profile.disabled_trends or [])+ except Exception:+ disabled_trends = set()++ active_slugs = [+ s for s in TREND_REGISTRY if s not in disabled_trends+ ]+ total_trends = len(active_slugs)+ self.stdout.write(+ f" {user} ({user.id}): {total_trends} trends ("+ f"{len(disabled_trends & set(TREND_REGISTRY.keys()))} disabled)..."+ ) user_start = timezone.now() user_ok = 0 user_fail = 0 - for idx, (slug, _) in enumerate(TREND_REGISTRY.items(), start=1):+ for idx, slug in enumerate(active_slugs, start=1): periods = get_supported_periods(slug) self.stdout.write(f" [{idx}/{total_trends}] {slug}...\n") for period in periods:@@ -76,7 +97,7 @@ class Command(BaseCommand): overall_elapsed = (timezone.now() - overall_start).total_seconds() self.stdout.write( self.style.SUCCESS(- f"Done! {ok_count} OK, {fail_count} failed "+ f"Done! {ok_count} OK, {fail_count} failed, {skipped_count} skipped " f"({overall_elapsed:.1f}s across {total_users} user(s))" ) )@@ -26,7 +26,17 @@ def compute_user_trends(user_id): logger.warning("User %s not found, skipping trends", user_id) return - total = len(TREND_REGISTRY)+ try:+ profile = user.profile+ if profile.trends_disabled:+ logger.info("User %s (%d) has trends disabled, skipping", user, user_id)+ return+ disabled = set(profile.disabled_trends or [])+ except Exception:+ disabled = set()++ active_slugs = [s for s in TREND_REGISTRY if s not in disabled]+ total = len(active_slugs) logger.info( "Computing %d trends for user %s (%d)", total,@@ -34,7 +44,7 @@ def compute_user_trends(user_id): user_id, ) - for idx, (slug, _) in enumerate(TREND_REGISTRY.items(), start=1):+ for idx, slug in enumerate(active_slugs, start=1): compute_single_trend.delay(user_id, slug) logger.info("Dispatched all %d trends for user %s (%d)", total, user, user_id)@@ -52,6 +62,21 @@ def compute_single_trend(user_id, slug): logger.warning("Unknown trend slug '%s' for user %d", slug, user_id) return + try:+ profile = user.profile+ if profile.trends_disabled:+ logger.info(+ "User %d has trends disabled, skipping '%s'", user_id, slug+ )+ return+ if slug in (profile.disabled_trends or []):+ logger.info(+ "User %d has trend '%s' disabled, skipping", user_id, slug+ )+ return+ except Exception:+ pass+ periods = get_supported_periods(slug) for period in periods:@@ -3,6 +3,10 @@ {% block title %}{{ trend.title }}{% endblock %} {% block lists %}+{% if trend_not_found %}+<div class="alert alert-warning">Trend not found.</div>++{% else %} <div class="row mb-3"> <div class="col-12"> <a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary mb-2">← All Trends</a>@@ -35,13 +39,21 @@ {% if computed_at %} <small class="text-muted">Last computed: {{ computed_at|date:"F j, Y H:i" }}</small> {% endif %}++ <div class="mt-2">+ <form method="post" action="{% url 'trends:trend-toggle-disabled' trend.slug %}" class="d-inline">+ {% csrf_token %}+ {% if trend_disabled %}+ <button type="submit" class="btn btn-sm btn-outline-success">Enable this trend</button>+ {% else %}+ <button type="submit" class="btn btn-sm btn-outline-danger">Disable this trend</button>+ {% endif %}+ </form>+ </div> </div> </div> -{% if trend_not_found %}-<div class="alert alert-warning">Trend not found.</div>--{% elif data is None %}+{% if data is None %} <div class="alert alert-info"> No data computed yet for this period. Trends are updated once daily, check back later. </div>@@ -85,5 +97,6 @@ {% elif trend.slug == "mood-weather" %} {% include "trends/_mood_weather.html" %} +{% endif %} {% endif %} {% endblock %}@@ -25,11 +25,23 @@ </a> </h5> <p class="card-text text-muted">{{ trend.description }}</p>- {% if trend.computed_at %}- <small class="text-muted">Last computed: {{ trend.computed_at|date:"M j, Y H:i" }}</small>- {% else %}- <span class="badge bg-warning text-dark">Pending</span>- {% endif %}+ <div class="d-flex justify-content-between align-items-center">+ <div>+ {% if trend.computed_at %}+ <small class="text-muted">Last computed: {{ trend.computed_at|date:"M j, Y H:i" }}</small>+ {% else %}+ <span class="badge bg-warning text-dark">Pending</span>+ {% endif %}+ </div>+ <form method="post" action="{% url 'trends:trend-toggle-disabled' trend.slug %}" class="m-0">+ {% csrf_token %}+ {% if trend.disabled %}+ <button type="submit" class="btn btn-sm btn-outline-success">Enable</button>+ {% else %}+ <button type="submit" class="btn btn-sm btn-outline-danger">Disable</button>+ {% endif %}+ </form>+ </div> </div> </div> </div>@@ -1,9 +1,18 @@ from django.urls import path-from trends.views import TrendDetailView, TrendListView+from trends.views import (+ ToggleTrendDisabledView,+ TrendDetailView,+ TrendListView,+) app_name = "trends" urlpatterns = [ path("trends/", TrendListView.as_view(), name="trends-home"), path("trends/<slug:trend_slug>/", TrendDetailView.as_view(), name="trend-detail"),+ path(+ "trends/<slug:trend_slug>/toggle/",+ ToggleTrendDisabledView.as_view(),+ name="trend-toggle-disabled",+ ), ]@@ -3,6 +3,7 @@ from datetime import timedelta from django.utils import timezone from trends.models import PERIOD_CHOICES, TrendResult+from trends.trends import TREND_REGISTRY logger = logging.getLogger(__name__) @@ -35,6 +36,13 @@ TREND_PERIOD_OVERRIDES = { } +def get_disabled_trends(user):+ profile = user.profile+ if profile.trends_disabled:+ return set(TREND_REGISTRY.keys())+ return set(profile.disabled_trends or [])++ def get_supported_periods(trend_slug): if trend_slug in TREND_PERIOD_OVERRIDES: slugs = TREND_PERIOD_OVERRIDES[trend_slug]@@ -1,8 +1,11 @@+from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin-from django.views.generic import TemplateView+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_period_nav, get_supported_periods+from trends.utils import get_disabled_trends, get_period_nav, get_supported_periods TREND_METADATA = { "activity-distribution": {@@ -78,6 +81,8 @@ class TrendListView(LoginRequiredMixin, TemplateView): 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")@@ -89,8 +94,11 @@ class TrendListView(LoginRequiredMixin, TemplateView): 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,@@ -99,6 +107,7 @@ class TrendListView(LoginRequiredMixin, TemplateView): "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@@ -148,4 +157,25 @@ class TrendDetailView(LoginRequiredMixin, TemplateView): 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"))+ )@@ -130,6 +130,11 @@ </ul> </div> {% endif %}+ {% elif field.name == "trends_disabled" %}+ <p class="checkbox-row">+ {{ field }}+ {{ field.label_tag }}+ </p> {% elif field.name == "home_scrobble_limit" %} <p> {{ field.label_tag }}@@ -76,9 +76,11 @@ <div class="btn-group me-2"> <a href="{% url 'scrobbles:scrobble-list' %}" class="btn btn-sm btn-outline-secondary">Live</a> </div>+ {% if not user.profile.trends_disabled %} <div class="btn-group me-2"> <a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary">Trends</a> </div>+ {% endif %} <div class="btn-group me-2"> {% if user.profile.lastfm_username and not user.profile.lastfm_auto_import %} <form action="{% url 'scrobbles:lastfm-import' %}" method="get">