sessions / 41a68291a4f10cf842396c64654247fba6bf924c

/home/powellc/src/code.unbl.ink/secstate/vrobbler

This commit has no recorded session.

diff

ignore whitespace

commit 41a68291a4f10cf842396c64654247fba6bf924cAuthor: Colin Powell <colin@unbl.ink>Date:   Thu Jun 25 18:58:23 2026 -0400    [trends] Allow disabling one or many or all trendsdiff --git a/PROJECT.org b/PROJECT.orgindex fb2b971..04fad1b 100644--- a/PROJECT.org+++ b/PROJECT.org@@ -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-4666a8d38ae4diff --git a/vrobbler/apps/profiles/forms.py b/vrobbler/apps/profiles/forms.pyindex 35e10d1..966a747 100644--- a/vrobbler/apps/profiles/forms.py+++ b/vrobbler/apps/profiles/forms.py@@ -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),diff --git a/vrobbler/apps/profiles/migrations/0040_userprofile_trends_disabled.py b/vrobbler/apps/profiles/migrations/0040_userprofile_trends_disabled.pynew file mode 100644index 0000000..af30c34--- /dev/null+++ b/vrobbler/apps/profiles/migrations/0040_userprofile_trends_disabled.py@@ -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),+        ),+    ]diff --git a/vrobbler/apps/profiles/models.py b/vrobbler/apps/profiles/models.pyindex 4dc467b..0c982b3 100644--- a/vrobbler/apps/profiles/models.py+++ b/vrobbler/apps/profiles/models.py@@ -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}" diff --git a/vrobbler/apps/trends/management/commands/compute_trends.py b/vrobbler/apps/trends/management/commands/compute_trends.pyindex 76e2027..30088a7 100644--- a/vrobbler/apps/trends/management/commands/compute_trends.py+++ b/vrobbler/apps/trends/management/commands/compute_trends.py@@ -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))"             )         )diff --git a/vrobbler/apps/trends/tasks.py b/vrobbler/apps/trends/tasks.pyindex d7e3fb7..260250d 100644--- a/vrobbler/apps/trends/tasks.py+++ b/vrobbler/apps/trends/tasks.py@@ -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:diff --git a/vrobbler/apps/trends/templates/trends/trend_detail.html b/vrobbler/apps/trends/templates/trends/trend_detail.htmlindex 3884243..20b028d 100644--- a/vrobbler/apps/trends/templates/trends/trend_detail.html+++ b/vrobbler/apps/trends/templates/trends/trend_detail.html@@ -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">&larr; 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 %}diff --git a/vrobbler/apps/trends/templates/trends/trend_list.html b/vrobbler/apps/trends/templates/trends/trend_list.htmlindex a204b3d..93d4bd6 100644--- a/vrobbler/apps/trends/templates/trends/trend_list.html+++ b/vrobbler/apps/trends/templates/trends/trend_list.html@@ -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>diff --git a/vrobbler/apps/trends/urls.py b/vrobbler/apps/trends/urls.pyindex f0c4071..c12cce8 100644--- a/vrobbler/apps/trends/urls.py+++ b/vrobbler/apps/trends/urls.py@@ -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",+    ), ]diff --git a/vrobbler/apps/trends/utils.py b/vrobbler/apps/trends/utils.pyindex 7f739cd..3fbf292 100644--- a/vrobbler/apps/trends/utils.py+++ b/vrobbler/apps/trends/utils.py@@ -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]diff --git a/vrobbler/apps/trends/views.py b/vrobbler/apps/trends/views.pyindex 8ad2e21..5a5298f 100644--- a/vrobbler/apps/trends/views.py+++ b/vrobbler/apps/trends/views.py@@ -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"))+        )diff --git a/vrobbler/templates/profiles/settings_form.html b/vrobbler/templates/profiles/settings_form.htmlindex e261d35..66a74f7 100644--- a/vrobbler/templates/profiles/settings_form.html+++ b/vrobbler/templates/profiles/settings_form.html@@ -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 }}diff --git a/vrobbler/templates/scrobbles/scrobble_list.html b/vrobbler/templates/scrobbles/scrobble_list.htmlindex 80fe0f2..a7ed0fe 100644--- a/vrobbler/templates/scrobbles/scrobble_list.html+++ b/vrobbler/templates/scrobbles/scrobble_list.html@@ -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">