feat: implement inline-editable fields and star rating widget#25
feat: implement inline-editable fields and star rating widget#25PascalRepond merged 1 commit intostagingfrom
Conversation
PascalRepond
commented
Dec 28, 2025
- Added inline-editable fields for review date, review text, score, and status in media item templates.
- Created a star rating widget for user reviews.
- Removed obsolete score-edit and score-readonly partials, replacing them with score-editable.
📝 WalkthroughWalkthroughThis pull request introduces a star rating widget component and implements HTMX-based inline editing for media attributes. It adds four new views that handle score, status, review, and review-date updates, replaces static display elements with editable partials, and removes deprecated templates. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser
participant MediaView as HTMX View
participant Form as Form/Validation
participant DB as Database
participant PartialTemplate as Partial Template
User->>Browser: Click edit or change value in partial
Browser->>MediaView: POST request with new value<br/>(e.g., media_update_score_htmx)
activate MediaView
MediaView->>Form: Validate input against<br/>field choices
alt Validation Success
Form-->>MediaView: Valid data
MediaView->>DB: Update media object
DB-->>MediaView: Object saved
else Validation Fails
Form-->>MediaView: Invalid data
MediaView-->>PartialTemplate: Render with error
end
MediaView->>PartialTemplate: Render updated partial<br/>with new value
PartialTemplate-->>Browser: Return updated HTML
deactivate MediaView
Browser->>Browser: HTMX swaps outerHTML
User->>User: Sees updated display/edit state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
src/core/templates/widgets/star_rating.html (2)
39-42: Consider simplifying the value comparison.The nested loop with
stringformatfilter to find the matching label could be simplified. Sincewidget.valueandscoreare both integers, you can compare them directly:- {% for score, label in score_choices %} - {% if widget.value|stringformat:"s" == score|stringformat:"s" %}{{ label }}{% endif %} - {% endfor %} + {% for score, label in score_choices %} + {% if widget.value == score %}{{ label }}{% endif %} + {% endfor %}
47-54: Add defensive checks for missing DOM elements.The script assumes all DOM elements exist. If the template structure changes or the widget fails to render properly, accessing properties on null elements will throw uncaught errors.
🔎 Suggested defensive initialization
(function() { // Initialize star rating for this widget instance const widgetName = "{{ widget.name }}"; const ratingContainer = document.querySelector(`.rating[data-widget-name="${widgetName}"]`); + if (!ratingContainer) return; + const hiddenInput = document.getElementById(`id_${widgetName}`); const labelDisplay = document.querySelector(`.star-label[data-widget-name="${widgetName}"]`); const clearBtn = document.querySelector(`.star-clear-btn[data-widget-name="${widgetName}"]`); + if (!hiddenInput || !labelDisplay || !clearBtn) return; + const stars = ratingContainer.querySelectorAll('.star-btn');src/templates/partials/status-editable.html (1)
21-36: Consider adding escape-to-cancel functionality.The widget switches to edit mode on click but doesn't provide a way to cancel editing without making a change. Users may accidentally click and then want to dismiss the edit mode without selecting a different status.
Consider adding an escape key handler or blur event to restore display mode, similar to the pattern used in
review-date-editable.html(lines 48-56).🔎 Suggested enhancement
// Show edit mode on click display.addEventListener('click', function() { display.classList.add('hidden'); edit.classList.remove('hidden'); select.focus(); }); + + // Cancel editing on escape or blur + select.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + edit.classList.add('hidden'); + display.classList.remove('hidden'); + } + }); + + select.addEventListener('blur', function() { + // Delay to allow HTMX change to process first + setTimeout(() => { + if (edit && !edit.classList.contains('hidden')) { + edit.classList.add('hidden'); + display.classList.remove('hidden'); + } + }, 300); + }); })();src/templates/partials/score-editable.html (2)
6-15: Consider reducing repetition in star button attributes.Each of the 10 star buttons shares identical
hx-post,hx-target, andhx-swapattributes, with onlydata-score,data-label,hx-vals, and the conditionalaria-currentdiffering. While functional, this repetition makes the template harder to maintain.Possible approaches to reduce repetition
- Use a template loop with a list of score/label pairs
- Move the common HTMX attributes to the container and use event delegation
- Accept the current approach for explicitness and clarity
Given the static nature of the 10-star scale, the current explicit approach is acceptable, but consider refactoring if the rating scale becomes configurable.
42-85: Inline JavaScript is duplicated for each media item.The script block is embedded directly in the template and will be repeated for every media item rendered on the page. With many items, this could impact page size and parsing performance.
Suggested optimization
Consider moving the hover interaction logic to a shared external JavaScript file that uses event delegation on a common parent or data attributes to identify the target widget. This would:
- Reduce HTML payload
- Improve browser caching
- Simplify maintenance
However, for typical list sizes, the current approach is acceptable and keeps the component self-contained.
src/templates/partials/review-editable.html (1)
48-121: Inline JavaScript duplicated per widget.Similar to the score widget, this script block is repeated for each media item. For pages with many items, consider extracting to a shared JavaScript file with event delegation.
However, the current self-contained approach is acceptable for typical list sizes and keeps the component easy to understand.
src/core/views.py (1)
284-303: Silent error suppression may hide unexpected issues.Lines 298-299 use
contextlib.suppress(ValueError, TypeError)to handle invalid date formats, which silently ignores errors. While this prevents the widget from breaking on invalid input, it provides no feedback to the user and could mask unexpected bugs (e.g., a malformed POST payload).Suggested improvement
Consider explicit error handling with logging or user feedback:
- with contextlib.suppress(ValueError, TypeError): - media.review_date = review_date_value + try: + media.review_date = review_date_value + except (ValueError, TypeError) as e: + # Log the error for debugging + # Optionally: add error message to response context + pass # Keep existing date on invalid inputThis maintains the same user experience while improving observability for debugging.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
pyproject.tomlsrc/core/forms.pysrc/core/models.pysrc/core/templates/widgets/star_rating.htmlsrc/core/urls.pysrc/core/views.pysrc/templates/partials/media-items.htmlsrc/templates/partials/review-date-editable.htmlsrc/templates/partials/review-editable.htmlsrc/templates/partials/score-edit.htmlsrc/templates/partials/score-editable.htmlsrc/templates/partials/score-readonly.htmlsrc/templates/partials/status-editable.htmlsrc/theme/static_src/src/styles.css
💤 Files with no reviewable changes (2)
- src/templates/partials/score-edit.html
- src/templates/partials/score-readonly.html
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 21
File: src/templates/accounts/profile_edit.html:23-58
Timestamp: 2025-12-26T15:18:46.932Z
Learning: In Django projects, when form field widgets have HTMX attributes added via `field.widget.attrs.update()` in the form's `__init__` method (as in src/accounts/forms.py), those attributes are automatically rendered when using `{{ form.field }}` in templates. This is a valid pattern and does not require explicit attribute definitions in the template.
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 23
File: src/templates/partials/score-readonly.html:2-33
Timestamp: 2025-12-27T17:59:56.022Z
Learning: In src/templates/partials/score-readonly.html, the DaisyUI read-only rating pattern correctly uses `aria-current="true"` on the selected star and applies the same color class (bg-orange-400) to all star elements. DaisyUI's CSS automatically handles which stars are visually filled based on the aria-current position.
📚 Learning: 2025-12-26T15:18:46.932Z
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 21
File: src/templates/accounts/profile_edit.html:23-58
Timestamp: 2025-12-26T15:18:46.932Z
Learning: In Django projects, attributes added to a form field's widget via field.widget.attrs.update(...) in the form's __init__ are rendered when using {{ form.field }} in templates. No explicit attribute definitions are needed in the template. This applies to templates under src/templates in Django apps; ensure you update attrs in __init__ for consistent HTMX behavior.
Applied to files:
src/templates/partials/media-items.htmlsrc/templates/partials/review-editable.htmlsrc/templates/partials/review-date-editable.htmlsrc/templates/partials/status-editable.htmlsrc/templates/partials/score-editable.html
📚 Learning: 2025-12-27T17:59:56.022Z
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 23
File: src/templates/partials/score-readonly.html:2-33
Timestamp: 2025-12-27T17:59:56.022Z
Learning: In src/templates/partials/score-readonly.html, the DaisyUI read-only rating pattern correctly uses `aria-current="true"` on the selected star and applies the same color class (bg-orange-400) to all star elements. DaisyUI's CSS automatically handles which stars are visually filled based on the aria-current position.
Applied to files:
src/core/templates/widgets/star_rating.htmlsrc/theme/static_src/src/styles.csssrc/templates/partials/score-editable.html
📚 Learning: 2025-12-26T15:18:46.932Z
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 21
File: src/templates/accounts/profile_edit.html:23-58
Timestamp: 2025-12-26T15:18:46.932Z
Learning: In Django projects, when form field widgets have HTMX attributes added via `field.widget.attrs.update()` in the form's `__init__` method (as in src/accounts/forms.py), those attributes are automatically rendered when using `{{ form.field }}` in templates. This is a valid pattern and does not require explicit attribute definitions in the template.
Applied to files:
src/core/templates/widgets/star_rating.htmlsrc/core/forms.pysrc/core/views.py
🧬 Code graph analysis (4)
src/templates/partials/review-editable.html (1)
src/core/migrations/0003_media_review_media_review_date_media_score_and_more.py (1)
Migration(9-41)
src/core/urls.py (1)
src/core/views.py (4)
media_update_score_htmx(224-247)media_update_status_htmx(251-268)media_update_review_htmx(272-281)media_update_review_date_htmx(285-303)
src/core/forms.py (1)
src/core/models.py (1)
Media(107-210)
src/core/views.py (2)
src/tests/conftest.py (1)
media(20-31)src/core/models.py (2)
Media(107-210)save(197-210)
🔇 Additional comments (14)
src/core/models.py (1)
206-206: Inline suppression removal relies on global SLF001 ignore.The removal of the
# noqa: SLF001comment here only works because SLF001 was added to the global ignore list inpyproject.toml. Accessingself.cover._fileis intentional and documented, making it a good candidate for an inline suppression rather than global rule disabling.If the global SLF001 ignore in
pyproject.tomlis reverted (as suggested in that file's review), restore the inline suppression here.src/theme/static_src/src/styles.css (1)
16-28: LGTM!The star rating widget styles provide appropriate visual feedback with smooth transitions. The scale transforms (1.1 on hover, 0.95 on active) create an intuitive interactive experience.
src/core/templates/widgets/star_rating.html (1)
68-132: LGTM!The event handlers are well-structured:
- Click updates the value and triggers HTMX validation via the
inputevent (consistent with the project's HTMX patterns per learnings).- Hover effects provide immediate visual feedback without persisting changes.
- The clear button properly resets all state.
preventDefault()is correctly used to prevent default button behavior.src/core/urls.py (1)
17-22: LGTM!The new HTMX endpoint URL patterns follow consistent naming conventions and align with the views implemented in
src/core/views.py. The URL structure is RESTful and the naming scheme (media_update_{field}_htmx) is clear and maintainable.src/core/forms.py (2)
14-31: LGTM!The
StarRatingWidgetimplementation is clean and follows Django widget conventions:
- Uses the standard
get_contextmethod to injectscore_choicesinto the template.- Leverages Django's
_metaAPI to retrieve field choices from the model, ensuring consistency.- The template path correctly references the new widget template.
54-54: LGTM!The integration of
StarRatingWidgetinto theMediaFormreplaces the previous select widget with the new star rating interface. Thevalidatorclass is correctly preserved to maintain HTMX validation behavior.src/templates/partials/review-date-editable.html (2)
11-24: LGTM! The blur delay coordination is well-designed.The combination of
hx-trigger="blur delay:200ms"on the input and the Today button's use ofhtmx.trigger(input, 'blur')creates a smooth UX. The delay allows the Today button click to complete before the blur event fires, ensuring the date is populated before submission.
27-66: LGTM! Comprehensive interaction handling.The JavaScript provides excellent UX:
- Escape key cancels editing without submission
- Today button populates and submits in one action
- Tooltip provides format guidance
- Proper class toggling for visibility states
src/templates/partials/score-editable.html (1)
1-85: Implementation looks solid overall.The star rating widget correctly:
- Uses
aria-currentfor accessibility (consistent with DaisyUI patterns per learnings)- Handles the Clear button for nullable scores
- Provides immediate visual feedback via hover states
- Uses
escapejs-like output for JavaScript safety (line 80:media.score|default:"null")The HTMX integration properly targets the widget for outerHTML replacement, maintaining state after updates.
src/templates/partials/media-items.html (1)
36-42: Clean integration of editable partials.The replacement of static display elements with editable partials is well-structured:
- Status includes
status_choicescontext variable for the dropdown (lines 38, 80)- Each partial is properly isolated and reusable
- The conditional rendering and loop structures remain intact
The modular approach makes it easy to maintain and update individual components independently.
Also applies to: 80-80, 84-87
src/templates/partials/review-editable.html (1)
33-36: Elegant single-save-path design.The textarea auto-saves on blur (line 33:
hx-trigger="blur delay:200ms"), and the Save button simply dispatches the blur event (line 109). This creates a consistent save mechanism with two intuitive triggers:
- Implicit: clicking outside the textarea
- Explicit: clicking Save
The 200ms delay prevents premature saves during rapid focus changes.
Also applies to: 106-111
src/core/views.py (3)
224-247: Score update validation is correct.The function properly:
- Handles empty string to clear the score (lines 232-234)
- Validates against model choices before updating (lines 239-240)
- Silently ignores invalid formats to prevent breaking the UI (lines 243-245)
This defensive approach ensures the widget remains functional even with unexpected input.
250-268: Status update includes required context.Unlike score and review updates, this view passes
status_choicesin the context (line 266), which matches the template's need for the dropdown options insrc/templates/partials/status-editable.html. Good consistency.
49-56: Good extraction of field choices helper.The
_get_field_choices()function (lines 49-56) centralizes access to model field choices and is consistently used in bothindex(line 100) andsearch_media(line 186) views. This ensures filter dropdowns and editable widgets have consistent options.Also applies to: 186-186
a61573e to
c0b6622
Compare
- Added inline-editable fields for review date, review text, score, and status in media item templates. - Created a star rating widget for user reviews. - Removed obsolete score-edit and score-readonly partials, replacing them with score-editable.
c0b6622 to
3594c0d
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/templates/partials/score-editable.html (1)
6-15: Consider refactoring to reduce repetition.The 10 star buttons have nearly identical markup with only the score value and label changing. Consider using a template loop for better maintainability.
🔎 Suggested refactor using template loop
{% for choice_value, choice_label in score_choices %} <button type="button" class="mask mask-star-2 bg-orange-400 cursor-pointer transition-transform hover:scale-110 star-btn" data-score="{{ choice_value }}" data-label="{{ choice_label }}" aria-label="{{ choice_value }} star{{ choice_value|pluralize }}" {% if media.score and choice_value <= media.score %}aria-current="true"{% endif %} hx-post="{% url 'media_update_score_htmx' media.id %}" hx-vals='{"score": "{{ choice_value }}"}' hx-target="#score-widget-{{ media.id }}" hx-swap="outerHTML"></button> {% endfor %}Note: You'll need to ensure
score_choicesis passed in the context (currently available via theStarRatingWidget.get_contextmethod).src/core/views.py (1)
234-243: Consider providing user feedback for validation errors.When score validation fails (invalid format or out-of-range), the view silently ignores the error. While this is acceptable for an HTMX pattern (the widget simply doesn't update), consider adding visual feedback for better UX.
For example, you could return an error message in the template context and display it in the widget:
try: score = int(score_value) valid_scores = dict(Media.score.field.choices).keys() if score in valid_scores: media.score = score media.save() else: return render(request, "partials/score-editable.html", { "media": media, "error": _("Invalid score value") }) except ValueError: return render(request, "partials/score-editable.html", { "media": media, "error": _("Score must be a number") })
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
src/core/forms.pysrc/core/templates/widgets/star_rating.htmlsrc/core/urls.pysrc/core/views.pysrc/templates/partials/media-items.htmlsrc/templates/partials/review-date-editable.htmlsrc/templates/partials/review-editable.htmlsrc/templates/partials/score-edit.htmlsrc/templates/partials/score-editable.htmlsrc/templates/partials/score-readonly.htmlsrc/templates/partials/status-editable.htmlsrc/theme/static_src/src/styles.css
💤 Files with no reviewable changes (2)
- src/templates/partials/score-readonly.html
- src/templates/partials/score-edit.html
🚧 Files skipped from review as they are similar to previous changes (3)
- src/core/urls.py
- src/core/templates/widgets/star_rating.html
- src/templates/partials/review-date-editable.html
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 23
File: src/templates/partials/score-readonly.html:2-33
Timestamp: 2025-12-27T17:59:56.022Z
Learning: In src/templates/partials/score-readonly.html, the DaisyUI read-only rating pattern correctly uses `aria-current="true"` on the selected star and applies the same color class (bg-orange-400) to all star elements. DaisyUI's CSS automatically handles which stars are visually filled based on the aria-current position.
📚 Learning: 2025-12-26T15:18:46.932Z
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 21
File: src/templates/accounts/profile_edit.html:23-58
Timestamp: 2025-12-26T15:18:46.932Z
Learning: In Django projects, when form field widgets have HTMX attributes added via `field.widget.attrs.update()` in the form's `__init__` method (as in src/accounts/forms.py), those attributes are automatically rendered when using `{{ form.field }}` in templates. This is a valid pattern and does not require explicit attribute definitions in the template.
Applied to files:
src/core/forms.pysrc/core/views.py
📚 Learning: 2025-12-26T15:18:46.932Z
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 21
File: src/templates/accounts/profile_edit.html:23-58
Timestamp: 2025-12-26T15:18:46.932Z
Learning: In Django projects, attributes added to a form field's widget via field.widget.attrs.update(...) in the form's __init__ are rendered when using {{ form.field }} in templates. No explicit attribute definitions are needed in the template. This applies to templates under src/templates in Django apps; ensure you update attrs in __init__ for consistent HTMX behavior.
Applied to files:
src/templates/partials/media-items.htmlsrc/templates/partials/score-editable.htmlsrc/templates/partials/status-editable.htmlsrc/templates/partials/review-editable.html
📚 Learning: 2025-12-27T17:59:56.022Z
Learnt from: PascalRepond
Repo: PascalRepond/datakult PR: 23
File: src/templates/partials/score-readonly.html:2-33
Timestamp: 2025-12-27T17:59:56.022Z
Learning: In src/templates/partials/score-readonly.html, the DaisyUI read-only rating pattern correctly uses `aria-current="true"` on the selected star and applies the same color class (bg-orange-400) to all star elements. DaisyUI's CSS automatically handles which stars are visually filled based on the aria-current position.
Applied to files:
src/templates/partials/score-editable.htmlsrc/theme/static_src/src/styles.css
🧬 Code graph analysis (3)
src/core/forms.py (2)
src/core/models.py (1)
Media(107-210)src/core/migrations/0007_alter_media_score.py (1)
Migration(6-18)
src/templates/partials/score-editable.html (3)
src/core/migrations/0005_media_cover_alter_media_score.py (1)
Migration(6-23)src/core/migrations/0007_alter_media_score.py (1)
Migration(6-18)src/tests/core/test_mediaform_htmx.py (1)
test_mediaform_htmx_attrs(7-20)
src/core/views.py (2)
src/tests/conftest.py (1)
media(20-31)src/core/models.py (2)
Media(107-210)save(197-210)
🪛 Ruff (0.14.10)
src/core/forms.py
28-28: Unused noqa directive (non-enabled: SLF001)
Remove unused noqa directive
(RUF100)
🔇 Additional comments (12)
src/theme/static_src/src/styles.css (1)
16-28: LGTM!The star rating widget styles provide appropriate visual feedback with smooth transitions. The scale transforms on hover and active states enhance the user experience.
src/core/forms.py (2)
14-30: LGTM! Clean widget implementation.The
StarRatingWidgetcorrectly injects score choices into the template context for rendering labels. The use of_meta.get_field()is a documented Django API pattern.
54-54: LGTM! Widget integration is correct.The
StarRatingWidgetis properly configured in theMediaForm.Meta.widgetsdictionary with the validator class.src/templates/partials/status-editable.html (1)
1-55: LGTM! Status editing widget is well-implemented.The widget correctly toggles between display and edit modes, handles keyboard events (Escape), and uses HTMX for inline updates. The 300ms blur delay appropriately allows HTMX request processing.
src/templates/partials/score-editable.html (1)
1-103: LGTM! Star rating widget is functional and well-integrated.The widget provides good UX with hover previews, keyboard support (Escape key), and HTMX-based updates. The JavaScript correctly manages state transitions and visual feedback.
src/templates/partials/media-items.html (2)
36-42: LGTM! Clean integration of editable widgets in grid view.The editable partials for status, score, review, and review-date are properly integrated, replacing the previous static displays.
80-87: LGTM! Clean integration of editable widgets in table view.The editable partials are correctly integrated into the table cells, maintaining consistency with the grid view implementation.
src/templates/partials/review-editable.html (1)
1-131: LGTM! Review editing widget is well-implemented.The widget handles both add and edit scenarios, includes truncation for long reviews, and properly uses the
escapejsfilter to prevent XSS. The HTMX integration with blur delay is appropriate.src/core/views.py (4)
221-245: LGTM! Score update view is well-implemented.The view correctly validates the score against model choices, handles empty values for clearing the score, and maintains data integrity with proper error handling.
248-266: LGTM! Status update view is correctly implemented.The view validates status values against model choices and properly renders the updated widget with the necessary context.
269-279: LGTM! Review update view is straightforward and correct.The view handles review text updates appropriately, trimming whitespace and persisting changes.
282-304: LGTM! Review date update handles PartialDateField correctly.The view appropriately handles empty values for clearing the date and lets the
PartialDateFieldhandle string-to-date conversion with proper exception handling.
| """ | ||
| context = super().get_context(name, value, attrs) | ||
| # Get the choices from the Media model's score field | ||
| score_field = Media._meta.get_field("score") # noqa: SLF001 |
There was a problem hiding this comment.
Remove unused noqa directive.
The # noqa: SLF001 directive is unnecessary because the SLF001 rule is not enabled in your Ruff configuration.
🔎 Proposed fix
- score_field = Media._meta.get_field("score") # noqa: SLF001
+ score_field = Media._meta.get_field("score")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| score_field = Media._meta.get_field("score") # noqa: SLF001 | |
| score_field = Media._meta.get_field("score") |
🧰 Tools
🪛 Ruff (0.14.10)
28-28: Unused noqa directive (non-enabled: SLF001)
Remove unused noqa directive
(RUF100)
🤖 Prompt for AI Agents
In src/core/forms.py around line 28, remove the unnecessary inline noqa comment
("# noqa: SLF001") after the score_field assignment; simply delete the trailing
noqa directive so the line reads only the assignment (score_field =
Media._meta.get_field("score")), ensuring no other changes are made to
formatting or logic.