From 5cb280c7582f606bae64200aa10c8047a3c00f14 Mon Sep 17 00:00:00 2001 From: Pascal Repond Date: Sun, 4 Jan 2026 13:07:06 +0100 Subject: [PATCH] feat(media): improve media detail and edit templates - Enhance the media detail view and editor with better UI elements. - Fix adding contributors beginning the same as existing ones. --- src/core/templates/widgets/star_rating.html | 40 ++- src/core/urls.py | 1 + src/core/views.py | 10 +- src/locale/fr/LC_MESSAGES/django.po | 195 ++++++---- .../js/{contributors.js => media_edit.js} | 24 +- src/templates/accounts/profile_edit.html | 14 +- src/templates/backup_manage.html | 12 +- src/templates/media_detail.html | 113 ++++++ src/templates/media_edit.html | 337 ++++++++++-------- src/templates/partials/confirm-modal.html | 28 ++ .../partials/media-contributors.html | 27 +- src/templates/partials/media-cover.html | 2 +- src/templates/partials/media-items.html | 42 ++- src/templates/partials/media-score-badge.html | 3 +- src/templates/partials/media-score-stars.html | 15 + src/tests/core/test_views.py | 64 ++++ src/theme/static_src/src/styles.css | 5 + 17 files changed, 666 insertions(+), 266 deletions(-) rename src/static/js/{contributors.js => media_edit.js} (87%) create mode 100644 src/templates/media_detail.html create mode 100644 src/templates/partials/confirm-modal.html create mode 100644 src/templates/partials/media-score-stars.html diff --git a/src/core/templates/widgets/star_rating.html b/src/core/templates/widgets/star_rating.html index fdd9165..6a4ff29 100644 --- a/src/core/templates/widgets/star_rating.html +++ b/src/core/templates/widgets/star_rating.html @@ -34,11 +34,18 @@ {# Label display for hover/selected score - fixed height to prevent layout shift #} -
{% if widget.value %} {% for score, label in score_choices %} - {% if widget.value == score %}{{ label }}{% endif %} + {% if widget.value|add:0 == score %} + + {{ score }} + {% load heroicons %} + {% heroicon_mini "star" class="fill-orange-400 h-4" %} + {{ label }} + + {% endif %} {% endfor %} {% endif %}
@@ -67,6 +74,21 @@ }); } + // Update label badge display + function updateLabelBadge(score, label) { + if (score && label) { + labelDisplay.innerHTML = ` + ${score} + + + + ${label} + `; + } else { + labelDisplay.innerHTML = ''; + } + } + // Handle star click stars.forEach(star => { star.addEventListener('click', function(e) { @@ -80,8 +102,8 @@ // Update visual state updateStars(parseInt(score)); - // Update label - labelDisplay.textContent = label; + // Update label badge + updateLabelBadge(score, label); // Trigger change event for HTMX validation hiddenInput.dispatchEvent(new Event('input', { bubbles: true })); @@ -96,7 +118,7 @@ updateStars(score); // Show label - labelDisplay.textContent = label; + updateLabelBadge(score, label); }); }); @@ -109,10 +131,10 @@ if (currentScore) { const selectedStar = ratingContainer.querySelector(`[data-score="${currentScore}"]`); if (selectedStar) { - labelDisplay.textContent = selectedStar.dataset.label; + updateLabelBadge(currentScore, selectedStar.dataset.label); } } else { - labelDisplay.textContent = ''; + updateLabelBadge(null, null); } }); @@ -126,8 +148,8 @@ // Clear visual state updateStars(null); - // Clear label - labelDisplay.textContent = ''; + // Clear label badge + updateLabelBadge(null, null); // Trigger change event for HTMX validation hiddenInput.dispatchEvent(new Event('input', { bubbles: true })); diff --git a/src/core/urls.py b/src/core/urls.py index c8e94d9..949b3c8 100644 --- a/src/core/urls.py +++ b/src/core/urls.py @@ -6,6 +6,7 @@ urlpatterns = [ path("", views.index, name="home"), path("media/add/", views.media_edit, name="media_add"), + path("media//", views.media_detail, name="media_detail"), path("media//edit/", views.media_edit, name="media_edit"), path("media//delete/", views.media_delete, name="media_delete"), path("search/", views.search_media, name="search"), diff --git a/src/core/views.py b/src/core/views.py index 0999320..5951f67 100644 --- a/src/core/views.py +++ b/src/core/views.py @@ -131,6 +131,14 @@ def index(request): return render(request, "media.html", context) +@login_required +def media_detail(request, pk): + """Display detailed view of a single media item.""" + media = get_object_or_404(Media, pk=pk) + context = {"media": media} + return render(request, "media_detail.html", context) + + @login_required def media_edit(request, pk=None): media = get_object_or_404(Media, pk=pk) if pk else None @@ -163,7 +171,7 @@ def media_edit(request, pk=None): removed_ids = before_contributor_ids - after_contributor_ids if removed_ids: delete_orphan_agents_by_ids(removed_ids) - return redirect("home") + return redirect("media_detail", pk=instance.pk) else: form = MediaForm(instance=media) context = {"media": media, "form": form} diff --git a/src/locale/fr/LC_MESSAGES/django.po b/src/locale/fr/LC_MESSAGES/django.po index 5fbc24a..9ef62bf 100644 --- a/src/locale/fr/LC_MESSAGES/django.po +++ b/src/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-04 10:28+0100\n" +"POT-Creation-Date: 2026-01-04 14:32+0100\n" "PO-Revision-Date: 2026-01-04 10:45+0100\n" "Last-Translator: Pascal Repond \n" "Language-Team: LANGUAGE \n" @@ -62,7 +62,7 @@ msgstr "Fichier image invalide ou corrompu." msgid "Name" msgstr "Nom" -#: src/core/models.py:113 src/templates/media_edit.html:46 +#: src/core/models.py:113 src/templates/media_edit.html:77 #: src/templates/partials/media-list.html:15 msgid "Title" msgstr "Titre" @@ -71,7 +71,7 @@ msgstr "Titre" msgid "Contributor" msgstr "Contributeur" -#: src/core/models.py:125 src/templates/media_edit.html:85 +#: src/core/models.py:125 src/templates/media_edit.html:115 msgid "Media type" msgstr "Type de média" @@ -107,7 +107,7 @@ msgstr "Spectacle/Performance" msgid "Broadcast (podcast, web series, etc.)" msgstr "Diffusion (podcast, série web, etc.)" -#: src/core/models.py:140 src/templates/media_edit.html:94 +#: src/core/models.py:140 src/templates/media_edit.html:135 msgid "External URI" msgstr "URI externe" @@ -115,9 +115,11 @@ msgstr "URI externe" msgid "" "Link to an external page about this media (e.g., official site, IMDb, " "Goodreads, etc.)" -msgstr "Lien vers une page externe à propos de ce média (par exemple, site officiel, IMDb, Goodreads, etc.)" +msgstr "" +"Lien vers une page externe à propos de ce média (par exemple, site officiel, " +"IMDb, Goodreads, etc.)" -#: src/core/models.py:148 src/templates/media_edit.html:103 +#: src/core/models.py:148 src/templates/media_edit.html:164 #: src/templates/partials/media-list.html:17 msgid "Status" msgstr "Statut" @@ -142,7 +144,7 @@ msgstr "En pause" msgid "Did not finish" msgstr "Pas fini" -#: src/core/models.py:161 src/templates/media_edit.html:112 +#: src/core/models.py:161 src/templates/media_edit.html:126 msgid "Release year" msgstr "Année de sortie" @@ -150,7 +152,7 @@ msgstr "Année de sortie" msgid "Year must be between -4000 and 2100." msgstr "L'année doit être comprise entre -4000 et 2100." -#: src/core/models.py:170 src/templates/media_edit.html:130 +#: src/core/models.py:170 src/templates/media_edit.html:197 #: src/templates/partials/media-list.html:16 msgid "Review" msgstr "Critique" @@ -200,15 +202,17 @@ msgid "Adored" msgstr "Coup de cœur" #: src/core/models.py:198 src/templates/media.html:46 -#: src/templates/media_edit.html:139 src/templates/partials/media-list.html:18 +#: src/templates/media_edit.html:173 src/templates/partials/media-list.html:18 msgid "Review date" msgstr "Date de la critique" #: src/core/models.py:201 msgid "Either a full date or just year and month, or only year." -msgstr "Soit une date complète, soit seulement l'année et le mois, ou uniquement l'année." +msgstr "" +"Soit une date complète, soit seulement l'année et le mois, ou uniquement " +"l'année." -#: src/core/models.py:204 src/templates/media_edit.html:36 +#: src/core/models.py:204 src/templates/media_edit.html:63 msgid "Cover image" msgstr "Image de couverture" @@ -240,24 +244,24 @@ msgstr "Télécharger la couverture :" msgid "Clear rating" msgstr "Retirer la note" -#: src/core/views.py:298 +#: src/core/views.py:306 #, python-format msgid "Backup creation failed: %(error)s" msgstr "Échec de la création du backup : %(error)s" -#: src/core/views.py:309 +#: src/core/views.py:317 msgid "No file selected" msgstr "Aucun fichier sélectionné" -#: src/core/views.py:313 +#: src/core/views.py:321 msgid "Invalid file format. Use a .tar.gz file" msgstr "Format de fichier invalide. Utilisez un fichier .tar.gz" -#: src/core/views.py:328 +#: src/core/views.py:336 msgid "Backup imported successfully! All data has been restored." msgstr "Backup importé avec succès ! Toutes les données ont été restaurées." -#: src/core/views.py:337 +#: src/core/views.py:345 #, python-format msgid "Backup import failed: %(error)s" msgstr "Échec de l'importation du backup : %(error)s" @@ -322,7 +326,7 @@ msgid "Confirm new password" msgstr "Confirmer le nouveau mot de passe" #: src/templates/accounts/profile_edit.html:147 -#: src/templates/backup_manage.html:148 +#: src/templates/backup_manage.html:150 msgid "Back to Home" msgstr "Retour à l'accueil" @@ -336,7 +340,9 @@ msgstr "Exporter la sauvegarde" #: src/templates/backup_manage.html:30 msgid "Create a complete archive of all your data (database + media files)." -msgstr "Créer une archive complète de toutes vos données (base de données + fichiers médias)." +msgstr "" +"Créer une archive complète de toutes vos données (base de données + fichiers " +"médias)." #: src/templates/backup_manage.html:35 msgid "Backup contents:" @@ -358,7 +364,7 @@ msgstr "Métadonnées (version, date de création)" msgid "Download Backup" msgstr "Télécharger la sauvegarde" -#: src/templates/backup_manage.html:58 src/templates/backup_manage.html:95 +#: src/templates/backup_manage.html:58 src/templates/backup_manage.html:97 msgid "Import Backup" msgstr "Importer la sauvegarde" @@ -382,76 +388,92 @@ msgstr "SUPPRIMER TOUTES vos données actuelles" msgid "and replace it with the backup data." msgstr "et les remplacer par les données de la sauvegarde." -#: src/templates/backup_manage.html:80 -msgid "" -"⚠️ WARNING: This action will DELETE ALL your current data and replace it with " -"the backup data.\\n\\nAre you absolutely sure you want to continue?" -msgstr "⚠️ ATTENTION : Cette action va SUPPRIMER TOUTES vos données actuelles et les remplacer par les données de la sauvegarde.\\n\\nÊtes-vous absolument sûr de vouloir continuer ?" - #: src/templates/backup_manage.html:84 msgid "Select a backup file (.tar.gz)" msgstr "Sélectionnez un fichier de sauvegarde (.tar.gz)" -#: src/templates/backup_manage.html:107 +#: src/templates/backup_manage.html:109 msgid "Backup Information" msgstr "Informations sur la sauvegarde" -#: src/templates/backup_manage.html:112 +#: src/templates/backup_manage.html:114 msgid "Backup Format" msgstr "Format de la sauvegarde" -#: src/templates/backup_manage.html:114 +#: src/templates/backup_manage.html:116 msgid "Backups are compressed archives in" msgstr "Les sauvegardes sont des archives compressées au format" -#: src/templates/backup_manage.html:116 +#: src/templates/backup_manage.html:118 msgid "format containing all your data and files." msgstr "contenant toutes vos données et fichiers." -#: src/templates/backup_manage.html:120 +#: src/templates/backup_manage.html:122 msgid "Automatic Backups" msgstr "Sauvegardes automatiques" -#: src/templates/backup_manage.html:122 +#: src/templates/backup_manage.html:124 msgid "" "An automatic backup is created daily at 1:00 AM. The 7 most recent backups " "are kept, older ones are automatically deleted." -msgstr "Une sauvegarde automatique est créée quotidiennement à 1h00. Les 7 sauvegardes les plus récentes sont conservées, les plus anciennes sont automatiquement supprimées." +msgstr "" +"Une sauvegarde automatique est créée quotidiennement à 1h00. Les 7 " +"sauvegardes les plus récentes sont conservées, les plus anciennes sont " +"automatiquement supprimées." -#: src/templates/backup_manage.html:126 +#: src/templates/backup_manage.html:128 msgid "Automatic Backup Location" msgstr "Emplacement de la sauvegarde automatique" -#: src/templates/backup_manage.html:128 +#: src/templates/backup_manage.html:130 msgid "In production (Docker):" msgstr "En production (Docker) :" -#: src/templates/backup_manage.html:130 +#: src/templates/backup_manage.html:132 msgid "In development:" msgstr "En développement :" -#: src/templates/backup_manage.html:134 +#: src/templates/backup_manage.html:136 msgid "Best Practices" msgstr "Bonnes pratiques" -#: src/templates/backup_manage.html:136 +#: src/templates/backup_manage.html:138 msgid "Regularly download your backups to external storage" msgstr "Téléchargez régulièrement vos sauvegardes sur un stockage externe" -#: src/templates/backup_manage.html:137 +#: src/templates/backup_manage.html:139 msgid "Test your backups periodically in a test instance" msgstr "Testez périodiquement vos sauvegardes dans une instance de test" -#: src/templates/backup_manage.html:138 +#: src/templates/backup_manage.html:140 msgid "" "Keep multiple backups to be able to restore from different points in time" -msgstr "Conservez plusieurs sauvegardes pour pouvoir restaurer à partir de différents points dans le temps" +msgstr "" +"Conservez plusieurs sauvegardes pour pouvoir restaurer à partir de " +"différents points dans le temps" -#: src/templates/backup_manage.html:172 +#: src/templates/backup_manage.html:155 +msgid "⚠️ WARNING: Destructive Action" +msgstr "⚠️ ATTENTION : Action destructive" + +#: src/templates/backup_manage.html:155 +msgid "" +"This action will DELETE ALL your current data and replace it with the backup " +"data. Are you absolutely sure you want to continue?" +msgstr "" +"Cette action va SUPPRIMER TOUTES vos données actuelles et les " +"remplacer par les données de la sauvegarde. Êtes-vous absolument sûr de " +"vouloir continuer ?" + +#: src/templates/backup_manage.html:155 +msgid "Yes, import backup" +msgstr "Oui, importer la sauvegarde" + +#: src/templates/backup_manage.html:176 msgid "Backup creation failed. Please try again." msgstr "Échec de la création de la sauvegarde. Veuillez réessayer." -#: src/templates/backup_manage.html:172 +#: src/templates/backup_manage.html:176 msgid "Unknown error" msgstr "Erreur inconnue" @@ -479,7 +501,8 @@ msgstr "Se déconnecter" msgid "Search" msgstr "Recherche" -#: src/templates/media.html:8 +#: src/templates/media.html:8 src/templates/media_detail.html:19 +#: src/templates/media_edit.html:29 msgid "My media" msgstr "Mes médias" @@ -487,7 +510,7 @@ msgstr "Mes médias" msgid "Creation date" msgstr "Date de création" -#: src/templates/media.html:47 src/templates/media_edit.html:121 +#: src/templates/media.html:47 src/templates/media_edit.html:154 msgid "Score" msgstr "Note" @@ -495,40 +518,81 @@ msgstr "Note" msgid "Add" msgstr "Ajouter" -#: src/templates/media_edit.html:6 src/templates/media_edit.html:29 +#: src/templates/media_detail.html:15 src/templates/media_edit.html:25 +msgid "Back to list" +msgstr "Retour à la liste" + +#: src/templates/media_detail.html:30 src/templates/media_edit.html:6 +#: src/templates/media_edit.html:33 src/templates/media_edit.html:43 #: src/templates/partials/media-edit-button.html:6 #: src/templates/partials/media-edit-button.html:7 msgid "Edit" msgstr "Modifier" -#: src/templates/media_edit.html:8 src/templates/media_edit.html:31 +#: src/templates/media_detail.html:41 src/templates/partials/media-cover.html:6 +#: src/templates/partials/media-items.html:12 +msgid "Cover of" +msgstr "Couverture de" + +#: src/templates/media_detail.html:73 +msgid "External metadata" +msgstr "Métadonnées externes" + +#: src/templates/media_edit.html:8 src/templates/media_edit.html:35 +#: src/templates/media_edit.html:45 msgid "Add media" msgstr "Ajouter un média" -#: src/templates/media_edit.html:25 -msgid "Close" -msgstr "Fermer" +#: src/templates/media_edit.html:51 +msgid "Save" +msgstr "Enregistrer" -#: src/templates/media_edit.html:55 +#: src/templates/media_edit.html:86 msgid "Contributors" msgstr "Contributeurs" -#: src/templates/media_edit.html:69 +#: src/templates/media_edit.html:98 msgid "Type name to search" msgstr "Tapez le nom à rechercher" -#: src/templates/media_edit.html:153 -msgid "" -"Are you sure you want to delete this media? This action cannot be undone." -msgstr "Êtes-vous sûr de vouloir supprimer ce média ? Cette action est irréversible." +#: src/templates/media_edit.html:177 +msgid "Set to today" +msgstr "Définir à aujourd'hui" -#: src/templates/media_edit.html:155 +#: src/templates/media_edit.html:179 +msgid "Today" +msgstr "Aujourd'hui" + +#: src/templates/media_edit.html:212 msgid "Delete" msgstr "Supprimer" -#: src/templates/media_edit.html:160 -msgid "Save" -msgstr "Enregistrer" +#: src/templates/media_edit.html:228 +msgid "Delete Media" +msgstr "Supprimer le média" + +#: src/templates/media_edit.html:228 +msgid "" +"Are you sure you want to delete this media? This action cannot be undone." +msgstr "" +"Êtes-vous sûr de vouloir supprimer ce média ? Cette action est irréversible." + +#: src/templates/media_edit.html:228 +msgid "Yes, delete" +msgstr "Oui, supprimer" + +#: src/templates/partials/confirm-modal.html:16 +msgid "Cancel" +msgstr "Annuler" + +#: src/templates/partials/confirm-modal.html:20 +#: src/templates/partials/confirm-modal.html:23 +msgid "Confirm" +msgstr "Confirmer" + +#: src/templates/partials/confirm-modal.html:27 +msgid "Close" +msgstr "Fermer" #: src/templates/partials/contributors-suggestions.html:17 msgid "No result, press Enter to add" @@ -563,11 +627,6 @@ msgstr "à" msgid "Loading more..." msgstr "Chargement..." -#: src/templates/partials/media-cover.html:6 -#: src/templates/partials/media-items.html:11 -msgid "Cover of" -msgstr "Couverture de" - #: src/templates/partials/media-list.html:14 msgid "Cover" msgstr "Couverture" @@ -588,6 +647,14 @@ msgstr "Voir plus" msgid "See less" msgstr "Voir moins" +#: src/templates/partials/media-score-stars.html:5 +msgid "Score:" +msgstr "Note :" + +#: src/templates/partials/media-score-stars.html:5 +msgid "out of 10" +msgstr "sur 10" + #: src/templates/partials/spinner.html:17 msgid "Loading..." msgstr "Chargement..." diff --git a/src/static/js/contributors.js b/src/static/js/media_edit.js similarity index 87% rename from src/static/js/contributors.js rename to src/static/js/media_edit.js index d5ec88f..71cd926 100644 --- a/src/static/js/contributors.js +++ b/src/static/js/media_edit.js @@ -1,5 +1,16 @@ -// Interactivity for contributor chips and autocomplete dropdown +// Interactivity for media edit page: contributors, date picker, and delete confirmation document.addEventListener('DOMContentLoaded', () => { + // Set up "Set to today" button for review date + const setTodayBtn = document.getElementById('set-today-btn'); + const reviewDateInput = document.getElementById('id_review_date'); + + if (setTodayBtn && reviewDateInput) { + setTodayBtn.addEventListener('click', () => { + reviewDateInput.value = new Date().toISOString().split('T')[0]; + }); + } + + // Contributor chips and autocomplete dropdown const input = document.getElementById('contributor_search'); const suggestions = document.getElementById('contributor-suggestions'); const chips = document.getElementById('contributors-chips'); @@ -26,12 +37,12 @@ document.addEventListener('DOMContentLoaded', () => { const contributorAlreadyExists = (name) => { const lower = name.trim().toLowerCase(); - // Cherche dans les chips existants (depuis la base de données) + // Check existing chips (from database) const existingChips = Array.from(chips?.querySelectorAll('span[data-id], span[data-name]') || []).some( (chip) => (chip.dataset.name || chip.textContent || '').trim().toLowerCase() === lower, ); - // Cherche dans les nouveaux contributeurs (créés dynamiquement) + // Check new contributors (created dynamically) const newContributors = Array.from(document.querySelectorAll('input[name="new_contributors"]')).some( (inp) => inp.value.trim().toLowerCase() === lower, ); @@ -67,13 +78,6 @@ document.addEventListener('DOMContentLoaded', () => { input.addEventListener('keydown', (evt) => { if (evt.key !== 'Enter') return; - const firstSuggestion = suggestions.querySelector('a'); - if (firstSuggestion) { - evt.preventDefault(); - firstSuggestion.click(); - return; - } - const name = input.value.trim(); if (!name) return; diff --git a/src/templates/accounts/profile_edit.html b/src/templates/accounts/profile_edit.html index 66c863f..534764f 100644 --- a/src/templates/accounts/profile_edit.html +++ b/src/templates/accounts/profile_edit.html @@ -27,7 +27,7 @@

{% translate "Profile Information" %}

{{ profile_form.username }} @@ -37,7 +37,7 @@

{% translate "Profile Information" %}

{{ profile_form.email }}
@@ -47,7 +47,7 @@

{% translate "Profile Information" %}

{{ profile_form.first_name }}
@@ -58,7 +58,7 @@

{% translate "Profile Information" %}

{{ profile_form.last_name }} @@ -111,7 +111,7 @@

{% translate "Change Password" %}

{{ password_form.old_password }} @@ -122,7 +122,7 @@

{% translate "Change Password" %}

{{ password_form.new_password1 }} @@ -133,7 +133,7 @@

{% translate "Change Password" %}

{{ password_form.new_password2 }} diff --git a/src/templates/backup_manage.html b/src/templates/backup_manage.html index 10f296d..8b5b041 100644 --- a/src/templates/backup_manage.html +++ b/src/templates/backup_manage.html @@ -74,10 +74,10 @@

{% translate "and replace it with the backup data." %} -
+ enctype="multipart/form-data"> {% csrf_token %}
- @@ -149,6 +151,8 @@

💡 {% translate "Best Practices" %}

+ {# Confirmation modal for backup import #} + {% include "partials/confirm-modal.html" with modal_id="confirm-import-modal" title=_("⚠️ WARNING: Destructive Action") message=_("This action will DELETE ALL your current data and replace it with the backup data. Are you absolutely sure you want to continue?") confirm_text=_("Yes, import backup") is_danger=True form_id="import-backup-form" %} -{% endblock content %} + {% if media %} + + {% csrf_token %} +
+ {# Confirmation modal for media deletion #} + {% include "partials/confirm-modal.html" with modal_id="confirm-delete-modal" title=_("Delete Media") message=_("Are you sure you want to delete this media? This action cannot be undone.") confirm_text=_("Yes, delete") is_danger=True form_id="delete-form" %} + {% endif %} + + {% endblock content %} diff --git a/src/templates/partials/confirm-modal.html b/src/templates/partials/confirm-modal.html new file mode 100644 index 0000000..418a734 --- /dev/null +++ b/src/templates/partials/confirm-modal.html @@ -0,0 +1,28 @@ +{% load i18n %} +{# Reusable confirmation modal #} +{# Expected parameters: #} +{# - modal_id: Unique modal ID #} +{# - title: Modal title #} +{# - message: Confirmation message #} +{# - confirm_text: Confirm button text (optional, default: "Confirm") #} +{# - is_danger: Boolean for danger style (optional, default: False) #} +{# - form_id: Form ID to submit (optional) #} + + diff --git a/src/templates/partials/media-contributors.html b/src/templates/partials/media-contributors.html index e5af5e8..c708cfc 100644 --- a/src/templates/partials/media-contributors.html +++ b/src/templates/partials/media-contributors.html @@ -1,14 +1,21 @@ -{# Renders the list of contributors with HTMX links #} -{# Parameters: media #} +{# Renders the list of contributors with links #} +{# Parameters: media, use_htmx (optional, default True if not set) #} {% if media.contributors.all %} {% for contributor in media.contributors.all %} - {{ contributor.name }}{% if not forloop.last %};{% endif %} + {% if use_htmx == False %} + {# Normal link to home page with contributor filter #} + {{ contributor.name }}{% if not forloop.last %};{% endif %} + {% else %} + {# HTMX link for in-page filtering (default) #} + {{ contributor.name }}{% if not forloop.last %};{% endif %} + {% endif %} {% endfor %} {% endif %} diff --git a/src/templates/partials/media-cover.html b/src/templates/partials/media-cover.html index ea20afa..758f188 100644 --- a/src/templates/partials/media-cover.html +++ b/src/templates/partials/media-cover.html @@ -1,6 +1,6 @@ {% load i18n %} {% load media_tags %} -
+
{% if media.cover %} {% translate
- {% if media.cover %} - {% translate - {% else %} -
- {% heroicon_outline "photo" class="w-24 h-24" %} -
- {% endif %} -
+ + {% if media.cover %} + {% translate + {% else %} +
+ {% 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" %}
+
{% include "partials/media-score-badge.html" %}
{% endif %}
-

{{ media.title }}

+ +

{{ media.title }}

+
{% if media.pub_year %}({{ media.pub_year }}){% endif %}
{% include "partials/media-contributors.html" %}
@@ -46,14 +50,18 @@

{{ media.title }}

{% for media in media_list %} {# Cover with media type badge #} - {% include "partials/media-cover.html" %} + + {% include "partials/media-cover.html" %} + {# Title, contributors #}
-

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

+ +

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

+
{% if media.contributors.all %}
{% include "partials/media-contributors.html" %}
{% endif %} diff --git a/src/templates/partials/media-score-badge.html b/src/templates/partials/media-score-badge.html index 074fd02..d4594fc 100644 --- a/src/templates/partials/media-score-badge.html +++ b/src/templates/partials/media-score-badge.html @@ -4,6 +4,5 @@ {{ media.score }} {% heroicon_mini "star" class="fill-orange-400 h-4" %} - - {{ media.get_score_display }} - +  {{ media.get_score_display }} {% endif %} diff --git a/src/templates/partials/media-score-stars.html b/src/templates/partials/media-score-stars.html new file mode 100644 index 0000000..8600ff7 --- /dev/null +++ b/src/templates/partials/media-score-stars.html @@ -0,0 +1,15 @@ +{% load i18n %} +{# Display score with stars (read-only) #} + diff --git a/src/tests/core/test_views.py b/src/tests/core/test_views.py index 6c586a6..49cd8b9 100644 --- a/src/tests/core/test_views.py +++ b/src/tests/core/test_views.py @@ -672,3 +672,67 @@ def test_load_more_includes_view_mode_in_context(self, logged_in_client, media_f assert response_list.context["view_mode"] == "list" assert response_grid.context["view_mode"] == "grid" + + +class TestMediaDetailView: + """Tests for the media_detail view.""" + + def test_media_detail_accessible_when_logged_in(self, logged_in_client, media): + """The detail view is accessible when logged in.""" + response = logged_in_client.get(reverse("media_detail", kwargs={"pk": media.pk})) + + assert response.status_code == 200 + assert response.context["media"] == media + + def test_media_detail_displays_correct_template(self, logged_in_client, media): + """The detail view uses the media_detail template.""" + response = logged_in_client.get(reverse("media_detail", kwargs={"pk": media.pk})) + + assert response.status_code == 200 + assert "media_detail.html" in [t.name for t in response.templates] + + def test_media_detail_nonexistent_returns_404(self, logged_in_client): + """Accessing detail view with nonexistent media returns 404.""" + response = logged_in_client.get(reverse("media_detail", kwargs={"pk": 99999})) + + assert response.status_code == 404 + + def test_media_detail_shows_all_fields(self, logged_in_client, db): + """The detail view displays all media fields.""" + agent = Agent.objects.create(name="Test Author") + media = Media.objects.create( + title="Complete Media", + media_type="BOOK", + status="COMPLETED", + score=8, + review="This is a detailed review.", + pub_year=2023, + external_uri="https://example.com", + ) + media.contributors.add(agent) + + response = logged_in_client.get(reverse("media_detail", kwargs={"pk": media.pk})) + content = response.content.decode("utf-8") + + assert media.title in content + assert "2023" in content + assert agent.name in content + assert "https://example.com" in content + + def test_media_detail_contributor_links_to_filtered_list(self, logged_in_client, db): + """Contributor links in detail view navigate to filtered home page.""" + agent = Agent.objects.create(name="Test Contributor") + media = Media.objects.create( + title="Test Media", + media_type="BOOK", + ) + media.contributors.add(agent) + + response = logged_in_client.get(reverse("media_detail", kwargs={"pk": media.pk})) + content = response.content.decode("utf-8") + + # Should contain a normal link (not HTMX) to home with contributor filter + expected_url = f'href="/?contributor={agent.id}"' + assert expected_url in content + # Should NOT contain HTMX attributes for contributor links + assert "hx-target" not in content diff --git a/src/theme/static_src/src/styles.css b/src/theme/static_src/src/styles.css index 5aeaad6..716e1c4 100644 --- a/src/theme/static_src/src/styles.css +++ b/src/theme/static_src/src/styles.css @@ -28,6 +28,11 @@ transform: scale(0.95); } +/* Form validation error styles */ +.label-text-alt { + @apply inline-block max-w-full break-words whitespace-normal; +} + /* Markdown content styles */ /* Uses DaisyUI/Tailwind utilities for theme consistency */ .review-text {