From d9a7a1cfd9156a599150a077fed967669005e61b Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 23 Mar 2026 10:40:23 -0400 Subject: [PATCH] [templates] Fix navigation and add AGENTS file --- AGENTS.md | 3 + .../charts/templates/charts/chart_index.html | 36 +++++ .../apps/charts/templatetags/chart_tags.py | 5 + vrobbler/apps/charts/views.py | 141 ++++++++++++++++++ .../apps/scrobbles/templatetags/form_tags.py | 8 +- vrobbler/templates/charts/chart_index.html | 19 +++ .../templates/scrobbles/_last_scrobbles.html | 9 +- .../templates/scrobbles/scrobble_list.html | 9 +- 8 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c3a4932 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +This is a Django-based web application that has an API, but primarily functions with traditional Django views with HTML templates to display data that mostly constitutes "scrobbled" items. The app started as a way to track a user's watched videos via a Jellyfin server, but has since grown to keep track of a number of media types: music tracks, tasks, videos, web pages, food, life events, sports events, podcasts, video games, board games, beers, brick (lego) sets, puzzles, books and geolocations. + +The project is written in Python and prefers to use "fat" models where logical methods are contained in either instance methods on instatiated data models, or classmethods on the Django model class itself. When logic grows too complex, helper functions should be pulled out into utils.py files and the model instance ro class method should call the utility function. diff --git a/vrobbler/apps/charts/templates/charts/chart_index.html b/vrobbler/apps/charts/templates/charts/chart_index.html index b4ffdc0..b3545a6 100644 --- a/vrobbler/apps/charts/templates/charts/chart_index.html +++ b/vrobbler/apps/charts/templates/charts/chart_index.html @@ -47,6 +47,42 @@ {% endblock %} {% block lists %} +
+
+ {% if prev_period %} + ← Previous + {% endif %} +
+
+ {{ period_type|title }} +
+
+ {% if next_period %} + Next → + {% endif %} +
+
+ {% if chart_years %} +
+ {% for y in chart_years|slice:":5" %} + {{ y }} + {% endfor %} +
+ {% endif %} +
+
+
+ Months: + {% for m in "1,2,3,4,5,6,7,8,9,10,11,12"|split:"," %} + {{ m }} + {% endfor %} + | + Weeks: + {% for w in "1,4,8,13,17,21,26,30,34,39,43,47,52"|split:"," %} + {{ w }} + {% endfor %} + + +
{% if chart_type == "maloja" %} {% include "scrobbles/_top_charts.html" %} {% else %} diff --git a/vrobbler/apps/charts/templatetags/chart_tags.py b/vrobbler/apps/charts/templatetags/chart_tags.py index 51ec232..b726fb3 100644 --- a/vrobbler/apps/charts/templatetags/chart_tags.py +++ b/vrobbler/apps/charts/templatetags/chart_tags.py @@ -8,3 +8,8 @@ def get_item(dictionary, key): if isinstance(dictionary, dict): return dictionary.get(key) return None + + +@register.filter +def split(value, arg): + return value.split(arg) diff --git a/vrobbler/apps/charts/views.py b/vrobbler/apps/charts/views.py index 812955a..d2a50b6 100644 --- a/vrobbler/apps/charts/views.py +++ b/vrobbler/apps/charts/views.py @@ -414,4 +414,145 @@ class ChartRecordView(TemplateView): ), } + 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): + return list( + ChartRecord.objects.filter(user=user) + .values_list("year", flat=True) + .distinct() + .order_by("-year") + ) + + 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 diff --git a/vrobbler/apps/scrobbles/templatetags/form_tags.py b/vrobbler/apps/scrobbles/templatetags/form_tags.py index 1203d9c..53c3d76 100644 --- a/vrobbler/apps/scrobbles/templatetags/form_tags.py +++ b/vrobbler/apps/scrobbles/templatetags/form_tags.py @@ -1,3 +1,5 @@ +from datetime import datetime + from django import template register = template.Library() @@ -5,7 +7,11 @@ register = template.Library() @register.filter(name="add_class") def add_class(field, css_class): - # If the widget is CheckboxInput, skip adding 'form-control' if field.field.widget.__class__.__name__ == "CheckboxInput": return field.as_widget() return field.as_widget(attrs={"class": css_class}) + + +@register.simple_tag +def current_date(format): + return datetime.now().strftime(format) diff --git a/vrobbler/templates/charts/chart_index.html b/vrobbler/templates/charts/chart_index.html index a8097e5..2efb8da 100644 --- a/vrobbler/templates/charts/chart_index.html +++ b/vrobbler/templates/charts/chart_index.html @@ -28,6 +28,25 @@ {% if week %}Week {{week}}{% endif %} {% if day %}{{day}}{% endif %} + + {% if period_type == "day" %} + {% if prev_period %}« Last day{% endif %} + {% if prev_period and next_period %} | {% endif %} + {% if next_period %}Next day »{% endif %} + {% elif period_type == "week" %} + {% if prev_period %}« Last week{% endif %} + {% if prev_period and next_period %} | {% endif %} + {% if next_period %}Next week »{% endif %} + {% elif period_type == "month" %} + {% if prev_period %}« Last month{% endif %} + {% if prev_period and next_period %} | {% endif %} + {% if next_period %}Next month »{% endif %} + {% elif period_type == "year" %} + {% if prev_period %}« Last year{% endif %} + {% if prev_period and next_period %} | {% endif %} + {% if next_period %}Next year »{% endif %} + {% endif %} + diff --git a/vrobbler/templates/scrobbles/_last_scrobbles.html b/vrobbler/templates/scrobbles/_last_scrobbles.html index 93725d6..51f38c2 100644 --- a/vrobbler/templates/scrobbles/_last_scrobbles.html +++ b/vrobbler/templates/scrobbles/_last_scrobbles.html @@ -1,8 +1,15 @@ {% load humanize %} {% load naturalduration %} +{% load form_tags %}
-

Today {{counts.today}} | This Week {{counts.week}} | This Month {{counts.month}} | This Year {{counts.year}} | All Time {{counts.alltime}}

+

+ Today {{counts.today}} | + This Week {{counts.week}} | + This Month {{counts.month}} | + This Year {{counts.year}} | + All Time {{counts.alltime}} +

diff --git a/vrobbler/templates/scrobbles/scrobble_list.html b/vrobbler/templates/scrobbles/scrobble_list.html index f6c21dd..7195955 100644 --- a/vrobbler/templates/scrobbles/scrobble_list.html +++ b/vrobbler/templates/scrobbles/scrobble_list.html @@ -2,6 +2,7 @@ {% load static %} {% load humanize %} {% load naturalduration %} +{% load form_tags %} {% block head_extra %}