From 1617651a09b8f420d86e2ed952df2ce2ccb002af Mon Sep 17 00:00:00 2001 From: Pascal Repond Date: Sat, 3 Jan 2026 18:30:45 +0100 Subject: [PATCH] feat(ui): enhance media list views display - Improved layout, colours and buttons for better user experience. - Created templatetags for media icons to standardize icon rendering across the app. - Updated relevant views and templates to integrate new features seamlessly. --- .vscode/settings.json | 1 + src/core/forms.py | 2 +- src/core/templatetags/__init__.py | 0 src/core/templatetags/media_tags.py | 75 +++++++++++ src/core/urls.py | 2 - src/core/views.py | 48 ------- src/templates/backup_manage.html | 1 - .../partials/media-contributors.html | 14 ++ src/templates/partials/media-cover.html | 16 +++ src/templates/partials/media-edit-button.html | 7 + src/templates/partials/media-icon.html | 1 + src/templates/partials/media-items.html | 127 ++++++++---------- src/templates/partials/media-list.html | 18 ++- .../partials/media-review-clamped.html | 6 +- src/templates/partials/media-review-full.html | 2 +- src/templates/partials/media-score-badge.html | 9 ++ .../partials/media-status-badge.html | 4 + .../partials/review-date-editable.html | 98 -------------- src/templates/partials/score-editable.html | 103 -------------- src/templates/partials/status-editable.html | 55 -------- src/tests/core/test_views.py | 6 +- 21 files changed, 197 insertions(+), 398 deletions(-) create mode 100644 src/core/templatetags/__init__.py create mode 100644 src/core/templatetags/media_tags.py create mode 100644 src/templates/partials/media-contributors.html create mode 100644 src/templates/partials/media-cover.html create mode 100644 src/templates/partials/media-edit-button.html create mode 100644 src/templates/partials/media-icon.html create mode 100644 src/templates/partials/media-score-badge.html create mode 100644 src/templates/partials/media-status-badge.html delete mode 100644 src/templates/partials/review-date-editable.html delete mode 100644 src/templates/partials/score-editable.html delete mode 100644 src/templates/partials/status-editable.html diff --git a/.vscode/settings.json b/.vscode/settings.json index 5e4fe5a..980596b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,4 +11,5 @@ "prettier.tabWidth": 2, "editor.tabSize": 2 }, + "djlint.ignore": "H006" } \ No newline at end of file diff --git a/src/core/forms.py b/src/core/forms.py index 8c43b9a..13394b9 100644 --- a/src/core/forms.py +++ b/src/core/forms.py @@ -59,7 +59,7 @@ class Meta: "review_date": forms.TextInput( attrs={ "class": "input validator w-full", - "placeholder": _("YYYY, MM-YYYY, or DD-MM-YYYY"), + "placeholder": _("YYYY, MM-YYYY, or YYYY-MM-DD"), } ), "cover": CoverImageWidget(attrs={"class": "file-input file-input-ghost w-full max-w-xs"}), diff --git a/src/core/templatetags/__init__.py b/src/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/templatetags/media_tags.py b/src/core/templatetags/media_tags.py new file mode 100644 index 0000000..542774d --- /dev/null +++ b/src/core/templatetags/media_tags.py @@ -0,0 +1,75 @@ +"""Custom template tags for media-related functionality.""" + +from django import template + +register = template.Library() + + +MEDIA_TYPE_ICONS = { + "BOOK": "book-open", + "GAME": "computer-desktop", + "MUSIC": "musical-note", + "COMIC": "book-open", + "FILM": "film", + "TV": "tv", + "PERF": "ticket", + "BROADCAST": "microphone", +} + +SIZE_CLASSES = { + "sm": "w-4 h-4", + "md": "w-6 h-6", + "lg": "w-8 h-8", +} + +STATUS_CLASSES = { + "PLANNED": "badge-info", + "IN_PROGRESS": "badge-accent", + "COMPLETED": "badge-success", + "PAUSED": "badge-neutral", + "DNF": "badge-error", +} + + +@register.inclusion_tag("partials/media-icon.html") +def media_icon(media_type, size="sm"): + """ + Render a heroicon based on media type. + + Args: + media_type: The type of media (BOOK, GAME, MUSIC, etc.) + size: Icon size (sm, md, lg) - default 'sm' + + Returns: + Context dict with icon_name and size_class + + Example usage: + {% load media_tags %} + {% media_icon media.media_type size="md" %} + """ + + icon_name = MEDIA_TYPE_ICONS.get(media_type, "question-mark-circle") + size_class = SIZE_CLASSES.get(size, "w-4 h-4") + + return { + "icon_name": icon_name, + "size_class": size_class, + } + + +@register.filter +def status_badge_class(status): + """ + Return the appropriate DaisyUI badge class for a given status. + + Args: + status: The media status (PLANNED, IN_PROGRESS, etc.) + + Returns: + String with DaisyUI badge class name + + Example usage: + + """ + + return STATUS_CLASSES.get(status, "badge-ghost") diff --git a/src/core/urls.py b/src/core/urls.py index b09953c..c8e94d9 100644 --- a/src/core/urls.py +++ b/src/core/urls.py @@ -15,8 +15,6 @@ path("media/validate_field/", validate_media_field, name="media_validate_field"), path("media//review-full/", views.media_review_full_htmx, name="media_review_full_htmx"), path("media//review-clamped/", views.media_review_clamped_htmx, name="media_review_clamped_htmx"), - path("media//update-score/", views.media_update_score_htmx, name="media_update_score_htmx"), - path("media//update-status/", views.media_update_status_htmx, name="media_update_status_htmx"), # Backup management path("backup/", views.backup_manage, name="backup_manage"), path("backup/export/", views.backup_export, name="backup_export"), diff --git a/src/core/views.py b/src/core/views.py index 68f99e9..0999320 100644 --- a/src/core/views.py +++ b/src/core/views.py @@ -280,54 +280,6 @@ def media_review_full_htmx(request, pk): return render(request, "partials/media-review-full.html", {"media": media}) -@login_required -def media_update_score_htmx(request, pk): - """HTMX view: update media score and return updated score widget.""" - media = get_object_or_404(Media, pk=pk) - - if request.method == "POST": - score_value = request.POST.get("score", "").strip() - - # Handle empty score (clear) - if score_value == "": - media.score = None - media.save() - else: - try: - score = int(score_value) - # Validate that score is one of the valid choices - valid_scores = dict(Media.score.field.choices).keys() - if score in valid_scores: - media.score = score - media.save() - except ValueError: - # Invalid score format, don't update - pass - - return render(request, "partials/score-editable.html", {"media": media}) - - -@login_required -def media_update_status_htmx(request, pk): - """HTMX view: update media status and return updated status widget.""" - media = get_object_or_404(Media, pk=pk) - - if request.method == "POST": - status_value = request.POST.get("status", "").strip() - - # Validate that status is one of the valid choices - valid_statuses = dict(Media.status.field.choices).keys() - if status_value in valid_statuses: - media.status = status_value - media.save() - - context = { - "media": media, - "status_choices": Media.status.field.choices, - } - return render(request, "partials/status-editable.html", context) - - @login_required def backup_export(request): """Export backup and download it.""" diff --git a/src/templates/backup_manage.html b/src/templates/backup_manage.html index 47c79f5..10f296d 100644 --- a/src/templates/backup_manage.html +++ b/src/templates/backup_manage.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load static %} -{% load heroicons %} {% load i18n %} {% block title %} {% translate "Backup Management" %} - Datakult diff --git a/src/templates/partials/media-contributors.html b/src/templates/partials/media-contributors.html new file mode 100644 index 0000000..e5af5e8 --- /dev/null +++ b/src/templates/partials/media-contributors.html @@ -0,0 +1,14 @@ +{# Renders the list of contributors with HTMX links #} +{# Parameters: media #} +{% if media.contributors.all %} + {% for contributor in media.contributors.all %} + {{ contributor.name }}{% if not forloop.last %};{% endif %} + {% endfor %} +{% endif %} diff --git a/src/templates/partials/media-cover.html b/src/templates/partials/media-cover.html new file mode 100644 index 0000000..ea20afa --- /dev/null +++ b/src/templates/partials/media-cover.html @@ -0,0 +1,16 @@ +{% load i18n %} +{% load media_tags %} +
+ {% if media.cover %} + {% translate + {% else %} +
+ {% heroicon_outline "photo" class="w-12 h-12" %} +
+ {% endif %} +
+ {% media_icon media.media_type size="sm" %} +
+
diff --git a/src/templates/partials/media-edit-button.html b/src/templates/partials/media-edit-button.html new file mode 100644 index 0000000..fbdd8d7 --- /dev/null +++ b/src/templates/partials/media-edit-button.html @@ -0,0 +1,7 @@ +{% load i18n %} +{# Renders the edit button for a media #} +{# Parameters: media #} +{% heroicon_outline "pencil-square" %} diff --git a/src/templates/partials/media-icon.html b/src/templates/partials/media-icon.html new file mode 100644 index 0000000..46eaebb --- /dev/null +++ b/src/templates/partials/media-icon.html @@ -0,0 +1 @@ +{% heroicon_outline icon_name class=size_class %} diff --git a/src/templates/partials/media-items.html b/src/templates/partials/media-items.html index 5becd65..b2bfc53 100644 --- a/src/templates/partials/media-items.html +++ b/src/templates/partials/media-items.html @@ -1,104 +1,85 @@ {% load i18n %} +{% load media_tags %} {# This partial renders only the media items without the container #} {% if view_mode == 'grid' %} - {# Grid items #} + {# Grid view - Cards #} {% for media in media_list %} -
-
+
+
{% if media.cover %} {{ media.title }} + alt="{% translate "Cover of" %} {{ media.title }}" + class="w-full h-full object-contain"> {% else %}
- {% heroicon_outline "photo" class="w-16 h-16" %} + {% heroicon_outline "photo" class="w-24 h-24" %}
{% endif %} +
+ {% media_icon media.media_type size="md" %} +
+ {% if media.score %} +
{% include "partials/media-score-badge.html" %}
+ {% endif %}
-
-

- {{ media.title }} -

-
- {% for contributor in media.contributors.all %} - {{ contributor.name }} - {% if not forloop.last %};{% endif %} - {% endfor %} -
- {{ media.get_media_type_display }} - {% include "partials/status-editable.html" with status_choices=status_choices %} +
+
+
+

{{ media.title }}

+ {% if media.pub_year %}({{ media.pub_year }}){% endif %} +
{% include "partials/media-contributors.html" %}
- {% include "partials/score-editable.html" %} - {% if media.review %} -
- {% include "partials/media-review-clamped.html" %} -
- {% endif %} - {% if media.review_date %} -
{{ media.review_date }}
- {% endif %} + {% include "partials/media-edit-button.html" %}
+ {% include "partials/media-status-badge.html" %} + {% if media.review %} +
+ {% include "partials/media-review-clamped.html" %} +
+ {% endif %}
{% endfor %} {% else %} - {# Table rows #} + {# List view - Table rows #} {% for media in media_list %} - - {% if media.cover %} - {% translate - {% else %} -
No cover
- {% endif %} - - - {{ media.title }} - - - {% for contributor in media.contributors.all %} - {{ contributor.name }} - {% if not forloop.last %};{% endif %} - {% endfor %} - - {{ media.get_media_type_display }} - - {% include "partials/status-editable.html" with status_choices=status_choices %} + {# Cover with media type badge #} + {% include "partials/media-cover.html" %} + {# Title, contributors #} + +
+

+ {{ media.title }} + {% if media.pub_year %}({{ media.pub_year }}){% endif %} +

+ {% if media.contributors.all %} +
{% include "partials/media-contributors.html" %}
+ {% endif %} +
- {{ media.pub_year | default:"" }} - - {% include "partials/score-editable.html" %} + {# Review #} + + {% include "partials/media-score-badge.html" with size="sm" extra_class="mb-2" %} {% if media.review %} -
+
{% include "partials/media-review-clamped.html" %}
{% endif %} - + {# Status #} + {% include "partials/media-status-badge.html" %} + {# Review date #} + {% if media.review_date %} -
{{ media.review_date }}
+ {{ media.review_date }} + {% else %} + {% endif %} + {# Actions #} + {% include "partials/media-edit-button.html" %} {% endfor %} {% endif %} diff --git a/src/templates/partials/media-list.html b/src/templates/partials/media-list.html index 7a95ac4..a9a0a14 100644 --- a/src/templates/partials/media-list.html +++ b/src/templates/partials/media-list.html @@ -3,22 +3,20 @@ {% if page_obj %} {% if view_mode == 'grid' %} {# Grid view - Cards layout #} -
{% include "partials/media-items-page.html" %}
{% else %} {# List view - Table layout #} -
+
- - - - - - - - + + + + + + diff --git a/src/templates/partials/media-review-clamped.html b/src/templates/partials/media-review-clamped.html index a4ed5c4..853526a 100644 --- a/src/templates/partials/media-review-clamped.html +++ b/src/templates/partials/media-review-clamped.html @@ -1,8 +1,8 @@ {# Display the review truncated with a 'See more' button (HTMX) #} {% load i18n %} -{{ media.review_rendered | truncatewords_html:20 | safe }} -{% if media.review|wordcount > 20 %} - diff --git a/src/templates/partials/media-review-full.html b/src/templates/partials/media-review-full.html index 021ee3b..18b33d3 100644 --- a/src/templates/partials/media-review-full.html +++ b/src/templates/partials/media-review-full.html @@ -1,7 +1,7 @@ {# Displays the full review for a media item (HTMX) #} {% load i18n %}
{{ media.review_rendered | safe }}
- diff --git a/src/templates/partials/media-score-badge.html b/src/templates/partials/media-score-badge.html new file mode 100644 index 0000000..074fd02 --- /dev/null +++ b/src/templates/partials/media-score-badge.html @@ -0,0 +1,9 @@ +{# Renders a score badge if score exists #} +{# Parameters: media, extra_class (optional) #} +{% if media.score %} + + {{ media.score }} + {% heroicon_mini "star" class="fill-orange-400 h-4" %} + - {{ media.get_score_display }} + +{% endif %} diff --git a/src/templates/partials/media-status-badge.html b/src/templates/partials/media-status-badge.html new file mode 100644 index 0000000..a814aaf --- /dev/null +++ b/src/templates/partials/media-status-badge.html @@ -0,0 +1,4 @@ +{% load media_tags %} +{# Renders a status badge #} +{# Parameters: media #} +{{ media.get_status_display }} diff --git a/src/templates/partials/review-date-editable.html b/src/templates/partials/review-date-editable.html deleted file mode 100644 index f9073f2..0000000 --- a/src/templates/partials/review-date-editable.html +++ /dev/null @@ -1,98 +0,0 @@ -{% load i18n %} -
-
- {% if media.review_date %} - {{ media.review_date }} - {% else %} - {% translate "No date" %} - {% endif %} -
- - -
- - diff --git a/src/templates/partials/score-editable.html b/src/templates/partials/score-editable.html deleted file mode 100644 index baea83f..0000000 --- a/src/templates/partials/score-editable.html +++ /dev/null @@ -1,103 +0,0 @@ -{% load i18n %} -
-
- {# Star rating container #} -
- - - - - - - - - - -
- - {# Clear button #} - {% if media.score %} - - {% endif %} -
- - {# Score label with fixed height #} -
- {% if media.score %} - {{ media.get_score_display }} - {% endif %} -
-
- - diff --git a/src/templates/partials/status-editable.html b/src/templates/partials/status-editable.html deleted file mode 100644 index 9c81c1c..0000000 --- a/src/templates/partials/status-editable.html +++ /dev/null @@ -1,55 +0,0 @@ -{% load i18n %} -
-
-
{{ media.get_status_display }}
-
- - -
- - diff --git a/src/tests/core/test_views.py b/src/tests/core/test_views.py index 1ece814..6c586a6 100644 --- a/src/tests/core/test_views.py +++ b/src/tests/core/test_views.py @@ -353,12 +353,12 @@ def test_media_review_clamped_with_short_review(self, logged_in_client, db): response = logged_in_client.get(reverse("media_review_clamped_htmx", kwargs={"pk": media.pk})) content = response.content.decode("utf-8") - # With less than 20 words, the 'See more' button should not appear + # With less than 50 words, the 'See more' button should not appear assert "See more" not in content def test_media_review_clamped_with_long_review(self, logged_in_client, db): """Clamped review with long text shows 'See more' button.""" - long_review = " ".join(["word"] * 30) # 30 words + long_review = " ".join(["word"] * 55) media = Media.objects.create( title="Test Media", media_type="BOOK", @@ -367,7 +367,7 @@ def test_media_review_clamped_with_long_review(self, logged_in_client, db): response = logged_in_client.get(reverse("media_review_clamped_htmx", kwargs={"pk": media.pk})) content = response.content.decode("utf-8") - # With more than 20 words, the 'See more' button should appear + # With more than 50 words, the 'See more' button should appear assert "See more" in content def test_media_review_full_returns_partial(self, logged_in_client, db):
{% translate "Cover" %}{% translate "Title" %}{% translate "Contributor" %}{% translate "Type" %}{% translate "Status" %}{% translate "Year" %}{% translate "Review" %}{% translate "Reviewed on" %}{% translate "Cover" %}{% translate "Title" %}{% translate "Actions" %}