Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ name: CI

on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]

jobs:
test:
Expand Down
2 changes: 1 addition & 1 deletion src/core/fixtures/sample_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@
"media_type": "MUSIC",
"status": "COMPLETED",
"pub_year": 2000,
"review": "Une expérimentation audacieuse qui a divisé les fans mais reste un chef-d'œuvre.",
"review": "Une expérimentation audacieuse qui a divisé les fans mais reste un chef-d'œuvre. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"score": 9,
"review_date": "2019",
"contributors": [6],
Expand Down
18 changes: 18 additions & 0 deletions src/core/migrations/0007_alter_media_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-27 15:55

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0006_alter_agent_created_at_alter_media_created_at'),
]

operations = [
migrations.AlterField(
model_name='media',
name='score',
field=models.IntegerField(blank=True, choices=[(1, 'Detested'), (2, 'Hated'), (3, 'Disliked'), (4, 'Not appreciated'), (5, 'Moderately appreciated'), (6, 'Appreciated'), (7, 'Enjoyed'), (8, 'Really enjoyed'), (9, 'Loved'), (10, 'Adored')], null=True, verbose_name='Review score'),
),
]
20 changes: 10 additions & 10 deletions src/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,16 @@ class Media(models.Model):
null=True,
blank=True,
choices={
1: _("1⭐ - Detested"),
2: _("2⭐ - Hated"),
3: _("3⭐ - Disliked"),
4: _("4⭐ - Not appreciated"),
5: _("5⭐ - Moderately appreciated"),
6: _("6⭐ - Appreciated"),
7: _("7⭐ - Enjoyed"),
8: _("8⭐ - Really enjoyed"),
9: _("9⭐ - Loved"),
10: _("10⭐ - Adored"),
1: _("Detested"),
2: _("Hated"),
3: _("Disliked"),
4: _("Not appreciated"),
5: _("Moderately appreciated"),
6: _("Appreciated"),
7: _("Enjoyed"),
8: _("Really enjoyed"),
9: _("Loved"),
10: _("Adored"),
},
)
review_date = PartialDateField(
Expand Down
2 changes: 2 additions & 0 deletions src/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@
path("agents/search-htmx/", views.agent_search_htmx, name="agent_search_htmx"),
path("agents/select-htmx/", views.agent_select_htmx, name="agent_select_htmx"),
path("media/validate_field/", validate_media_field, name="media_validate_field"),
path("media/<int:pk>/review-full/", views.media_review_full_htmx, name="media_review_full_htmx"),
path("media/<int:pk>/review-clamped/", views.media_review_clamped_htmx, name="media_review_clamped_htmx"),
]
14 changes: 14 additions & 0 deletions src/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,17 @@ def agent_select_htmx(request):
return render(request, "partials/contributor-chip.html", {"agent": agent})
except Agent.DoesNotExist:
return render(request, "partials/contributor-chip.html", {"agent": None, "error": "Agent not found"})


@login_required
def media_review_clamped_htmx(request, pk):
"""HTMX view: return clamped review for a media item (for table cell collapse)."""
media = get_object_or_404(Media, pk=pk)
return render(request, "partials/media-review-clamped.html", {"media": media})


@login_required
def media_review_full_htmx(request, pk):
"""HTMX view: return full review for a media item (for table cell expansion)."""
media = get_object_or_404(Media, pk=pk)
return render(request, "partials/media-review-full.html", {"media": media})
2 changes: 1 addition & 1 deletion src/static/js/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function createFilterBadge(filterName, displayText, badgeClass = 'badge-secondar
badge.dataset.filter = filterName;
badge.innerHTML = `
<span>${displayText}</span>
<button type="button" class="hover:opacity-70 filter-badge-remove" data-filter="${filterName}">
<button type="button" class="btn btn-ghost btn-xs btn-circle filter-badge-remove" data-filter="${filterName}">
</button>
`;
Expand Down
2 changes: 1 addition & 1 deletion src/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{% tailwind_css %}
{% htmx_script %}
</head>
<body hx-headers="{'x-csrftoken': '{ csrf_token }}'}" class="mx-auto">
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' class="mx-auto">
<div>
{% block navbar %}
<div class="navbar bg-base-100 shadow-sm">
Expand Down
10 changes: 5 additions & 5 deletions src/templates/media.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ <h1 class="text-4xl my-4">{% translate "My media" %}</h1>
<div class="badge badge-secondary gap-1" data-filter="type">
<span>{{ filters.type }}</span>
<button type="button"
class="hover:text-secondary-content/70 filter-badge-remove"
class="btn btn-ghost btn-xs btn-circle filter-badge-remove"
data-filter="type">{% heroicon_mini "x-mark" class="w-4 h-4" %}</button>
</div>
{% endif %}
{% if filters.status %}
<div class="badge badge-secondary gap-1" data-filter="status">
<span>{{ filters.status }}</span>
<button type="button"
class="hover:text-secondary-content/70 filter-badge-remove"
class="btn btn-ghost btn-xs btn-circle filter-badge-remove"
data-filter="status">{% heroicon_mini "x-mark" class="w-4 h-4" %}</button>
</div>
{% endif %}
Expand All @@ -90,7 +90,7 @@ <h1 class="text-4xl my-4">{% translate "My media" %}</h1>
{% if filters.score != 'none' %}⭐{% endif %}
</span>
<button type="button"
class="hover:text-secondary-content/70 filter-badge-remove"
class="btn btn-ghost btn-xs btn-circle filter-badge-remove"
data-filter="score">{% heroicon_mini "x-mark" class="w-4 h-4" %}</button>
</div>
{% endif %}
Expand All @@ -100,7 +100,7 @@ <h1 class="text-4xl my-4">{% translate "My media" %}</h1>
{% if filters.review_from and filters.review_to %}→{% endif %}
{{ filters.review_to }}</span>
<button type="button"
class="hover:text-secondary-content/70 filter-badge-remove"
class="btn btn-ghost btn-xs btn-circle filter-badge-remove"
data-filter="review">{% heroicon_mini "x-mark" class="w-4 h-4" %}</button>
</div>
{% endif %}
Expand All @@ -110,7 +110,7 @@ <h1 class="text-4xl my-4">{% translate "My media" %}</h1>
id="contributor-badge">
<span id="contributor-badge-name">{{ contributor.name }}</span>
<button type="button"
class="hover:text-primary-content/70 filter-badge-remove"
class="btn btn-ghost btn-xs btn-circle filter-badge-remove"
data-filter="contributor">{% heroicon_mini "x-mark" class="w-4 h-4" %}</button>
</div>
{% endif %}
Expand Down
56 changes: 26 additions & 30 deletions src/templates/partials/media-items.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,39 @@
{% if view_mode == 'grid' %}
{# Grid items #}
{% for media in media_list %}
<div class="card bg-base-200 shadow-md hover:shadow-lg transition-shadow">
<figure class="aspect-2/3 bg-base-300">
<div class="card lg:card-side bg-base-200 shadow-md hover:shadow-lg transition-shadow">
<figure class="basis-1/4 shrink-0 grow-0 h-full bg-base-300">
{% if media.cover %}
<img src="{{ media.cover.url }}"
alt="{{ media.title }}"
width="300"
height="450"
width="200"
height="300"
class="w-full h-full object-cover">
{% else %}
<div class="w-full h-full flex items-center justify-center text-base-content/30">
{% heroicon_outline "photo" class="w-16 h-16" %}
</div>
{% endif %}
</figure>
<div class="card-body p-3">
<div class="card-body basis-3/4 p-3 h-full">
<h3 class="card-title text-sm line-clamp-2">
<a href="{% url 'media_edit' media.id %}" class="link link-hover">{{ media.title }}</a>
</h3>
<div class="text-xs space-y-1">
<div class="flex justify-between items-center">
<span class="badge badge-sm">{{ media.get_media_type_display }}</span>
{% if media.score %}<span class="font-bold">{{ media.score }}/10</span>{% endif %}
</div>
{% if media.contributors.all %}
<p class="text-base-content/70 truncate">
{% for contributor in media.contributors.all %}
{{ contributor.name }}
{% if not forloop.last %},{% endif %}
{% endfor %}
</p>
{% endif %}
{% for contributor in media.contributors.all %}
<a href="#"
class="link link-secondary contributor-link"
data-contributor-id="{{ contributor.id }}"
data-contributor-name="{{ contributor.name }}"
hx-get="{% url 'home' %}?contributor={{ contributor.id }}"
hx-target="#media-list"
hx-include="#view-mode-input, #sort, #type, #status, #score, #review-from, #review-to"
hx-push-url="true">{{ contributor.name }}</a>
{% if not forloop.last %};{% endif %}
{% endfor %}
<span class="badge badge-sm block mt-1">{{ media.get_media_type_display }}</span>
{% include "partials/score-readonly.html" %}
<div id="review-cell-{{ media.id }}">{% include "partials/media-review-clamped.html" %}</div>
</div>
</div>
</div>
Expand All @@ -56,7 +58,7 @@ <h3 class="card-title text-sm line-clamp-2">
<td>
<a href="{% url 'media_edit' media.id %}" class="link link-secondary">{{ media.title }}</a>
</td>
<td>
<td class="max-w-xs">
{% for contributor in media.contributors.all %}
<a href="#"
class="link link-secondary contributor-link"
Expand All @@ -66,25 +68,19 @@ <h3 class="card-title text-sm line-clamp-2">
hx-target="#media-list"
hx-include="#view-mode-input, #sort, #type, #status, #score, #review-from, #review-to"
hx-push-url="true">{{ contributor.name }}</a>
{% if not forloop.last %},{% endif %}
{% if not forloop.last %};{% endif %}
{% endfor %}
</td>
<td>{{ media.get_media_type_display }}</td>
<td>
<div class="badge badge-soft badge-primary">{{ media.get_status_display }}</div>
</td>
<td>{{ media.pub_year | default:"" }}</td>
<td>{% include "partials/score-readonly.html" %}</td>
<td>{{ media.created_at | date:"Y-m-d" }}</td>
<td class="max-w-md">
{% include "partials/score-readonly.html" %}
<div id="review-cell-{{ media.id }}">{% include "partials/media-review-clamped.html" %}</div>
</td>
<td>{{ media.review_date | default:"" }}</td>
</tr>
{% endfor %}
{% endif %}
{# Load more button - replaces itself when clicked #}
{% if media_list.has_next %}
<div id="load-more-trigger" class="col-span-full">
<button class="btn btn-outline btn-block mt-4"
hx-get="?page={{ media_list.next_page_number }}&view_mode={{ view_mode }}&filter_type={{ filter_type }}&order_by={{ order_by }}"
hx-target="#load-more-trigger"
hx-swap="outerHTML">{% translate "Load more" %}</button>
</div>
{% endif %}
8 changes: 4 additions & 4 deletions src/templates/partials/media-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
{% if media_list %}
{% if view_mode == 'grid' %}
{# Grid view - Cards layout #}
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 mt-4"
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mt-4"
id="media-container">{% include "partials/media-items.html" %}</div>
{% else %}
{# List view - Table layout #}
<div class="overflow-x-auto">
<table class="table table-zebra table-pin-rows" id="media-container">
<table class="table table-zebra" id="media-container">
<thead>
<tr>
<th>{% translate "Cover" %}</th>
Expand All @@ -17,8 +17,8 @@
<th>{% translate "Type" %}</th>
<th>{% translate "Status" %}</th>
<th>{% translate "Year" %}</th>
<th>{% translate "Score" %}</th>
<th>{% translate "Created at" %}</th>
<th>{% translate "Review" %}</th>
<th>{% translate "Reviewed on" %}</th>
</tr>
</thead>
<tbody>
Expand Down
9 changes: 9 additions & 0 deletions src/templates/partials/media-review-clamped.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{# Display the review truncated to 3 lines with a 'See more' button (HTMX) #}
{% load i18n %}
{{ media.review | truncatechars:120 }}
{% if media.review and media.review|length > 120 %}
<button class="btn btn-xs btn-ghost mt-1"
hx-get="{% url 'media_review_full_htmx' media.id %}"
hx-target="#review-cell-{{ media.id }}"
hx-swap="innerHTML">{% translate "See more" %}</button>
{% endif %}
7 changes: 7 additions & 0 deletions src/templates/partials/media-review-full.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{# Displays the full review for a media item (HTMX) #}
{% load i18n %}
<div class="whitespace-pre-line">{{ media.review }}</div>
<button class="btn btn-xs btn-ghost mt-1"
hx-get="{% url 'media_review_clamped_htmx' media.id %}"
hx-target="#review-cell-{{ media.id }}"
hx-swap="innerHTML">{% translate "See less" %}</button>
30 changes: 12 additions & 18 deletions src/templates/partials/score-readonly.html
Original file line number Diff line number Diff line change
@@ -1,40 +1,34 @@
{% load i18n %}
<div class="text-xs">
{% if media.score %}
{{ media.get_score_display }}
{% else %}
{% translate "Not rated" %}
{% endif %}
</div>
<div class="rating rating-sm">
<div class="mask mask-star bg-primary"
<div class="rating rating-xs">
<div class="mask mask-star-2 bg-orange-400"
aria-label="1 star"
{% if media.score == 1 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="2 star"
{% if media.score == 2 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="3 star"
{% if media.score == 3 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="4 star"
{% if media.score == 4 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="5 star"
{% if media.score == 5 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="6 star"
{% if media.score == 6 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="7 star"
{% if media.score == 7 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="8 star"
{% if media.score == 8 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="9 star"
{% if media.score == 9 %}aria-current="true"{% endif %}></div>
<div class="mask mask-star bg-primary"
<div class="mask mask-star-2 bg-orange-400"
aria-label="10 star"
{% if media.score == 10 %}aria-current="true"{% endif %}></div>
</div>
Comment thread
PascalRepond marked this conversation as resolved.
{% if media.score %}<div class="badge badge-neutral badge-sm">{{ media.get_score_display }}</div>{% endif %}
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.