feat: add lazy loading for media list#29
Conversation
PascalRepond
commented
Dec 31, 2025
- Implemented lazy loading for media items on the media list page to enhance performance and user experience.
- Created new partial templates: load-more-trigger.html and media-items-page.html to support lazy loading functionality.
📝 WalkthroughWalkthroughAdds python-dotenv; loads .env and resolves NPM_BIN_PATH/NVM path in settings. Implements pagination (20 items/page) and an HTMX-enabled load-more endpoint with template partials for page fragments and a parameterized spinner. Updates templates to use page-based includes and adds tests covering pagination and load-more behavior. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Browser (HTMX)
participant View as load_more_media View
participant DB as Database
participant Tpl as Template Engine
participant Resp as HTTP Response
Browser->>View: GET /load-more/?page=N (& filters, sort, search)
activate View
View->>DB: Query media items (filters + sort + search)
activate DB
DB-->>View: Page N media items
deactivate DB
View->>View: Paginate (20/page), build context (page_obj, view_mode, filters, sort)
Note over View: determine page_obj.has_next
View->>Tpl: Render partial (media-items-page.html)
activate Tpl
Tpl->>Tpl: Render media-items.html (current items)
Tpl->>Tpl: Render load-more-trigger.html (if has_next, include state inputs)
Tpl-->>Resp: HTML fragment
deactivate Tpl
Resp-->>Browser: HTML fragment (items + load-more trigger)
Browser->>Browser: HTMX swaps/append into DOM
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/templates/partials/media-list.html (1)
3-3: Consider simplifying the condition.The condition
{% if media_list or page_obj %}may be redundant since, based on the context provided byload_more_mediaview, bothmedia_list(set topage_obj.object_list) andpage_objare always provided together. Consider simplifying to{% if page_obj %}sincepage_objis the primary pagination object.🔎 Proposed simplification
- {% if media_list or page_obj %} + {% if page_obj %}src/tests/core/test_views.py (1)
618-720: Thorough test coverage for the load_more_media view.The tests comprehensively cover authentication, template rendering, pagination mechanics, and state preservation (sorting, filters, search, view_mode). The multi-page scenarios (45 items across 3 pages) and last-page boundary conditions are well tested.
Consider adding an edge-case test for invalid page numbers (e.g.,
page=999orpage=0). Django'sPaginator.get_page()handles these gracefully by returning the last or first page, but explicit tests document this expected behavior.src/core/views.py (1)
175-215: Well-structured HTMX view for infinite scrolling.The view correctly handles all query parameters (search, sorting, filters, view_mode) and integrates with existing helper functions. The pagination logic mirrors the index view appropriately.
The search query filter (lines 185-190) duplicates the logic in
search_media(lines 225-230). Consider extracting to a helper function:🔎 Proposed refactor
def _build_search_queryset(query): """Build a filtered queryset based on search query.""" return Media.objects.filter( Q(title__icontains=query) | Q(contributors__name__icontains=query) | Q(pub_year__icontains=query) | Q(review__icontains=query), ).distinct()Then use in both views:
# In load_more_media and search_media: queryset = _build_search_queryset(query) if query else Media.objects.all()
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
pyproject.tomlsrc/config/settings.pysrc/core/urls.pysrc/core/views.pysrc/templates/media.htmlsrc/templates/partials/load-more-trigger.htmlsrc/templates/partials/media-items-page.htmlsrc/templates/partials/media-list.htmlsrc/templates/partials/spinner.htmlsrc/tests/core/test_views.pysrc/theme/static_src/src/styles.css
🧰 Additional context used
🧠 Learnings (2)
📚 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-page.htmlsrc/templates/partials/load-more-trigger.htmlsrc/templates/media.htmlsrc/templates/partials/media-list.htmlsrc/templates/partials/spinner.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/theme/static_src/src/styles.css
🧬 Code graph analysis (3)
src/core/views.py (2)
src/core/models.py (1)
Media(108-226)src/tests/conftest.py (1)
media(20-31)
src/core/urls.py (1)
src/core/views.py (1)
load_more_media(176-215)
src/tests/core/test_views.py (1)
src/tests/conftest.py (2)
logged_in_client(65-68)media_factory(35-49)
🔇 Additional comments (15)
src/theme/static_src/src/styles.css (1)
15-15: LGTM! Good practice to exclude node_modules from Tailwind scanning.Excluding
node_modulesfrom the Tailwind CSS build scan improves build performance and prevents third-party dependencies from being processed.src/core/urls.py (1)
12-12: LGTM! Clean URL pattern for infinite scroll endpoint.The route is well-positioned and properly connects to the
load_more_mediaview for HTMX-based pagination.src/templates/media.html (1)
63-63: LGTM! Proper HTMX indicator configuration.The parameterized spinner with
id="spinner"correctly aligns with thehx-indicator="#spinner"attribute on line 60, enabling proper loading state visualization during search requests.src/templates/partials/media-list.html (2)
27-27: LGTM! Consistent template usage across view modes.Switching the table view to use
media-items-page.htmlmaintains consistency with the grid view and properly supports infinite scrolling for both display modes.
7-9: LGTM! Proper HTMX infinite scroll implementation using self-replacing trigger pattern.The
id="media-container"correctly identifies the grid/table container for styling. The switch tomedia-items-page.htmlcorrectly includes bothmedia-items.htmlandload-more-trigger.html, which implements infinite scroll by fetching and replacing the trigger element itself (hx-swap="outerHTML"). Each response includes the next page's items plus a new load-more-trigger, creating a self-perpetuating infinite scroll pattern.src/templates/partials/media-items-page.html (1)
1-4: LGTM! Standard composition pattern for infinite scroll.The template correctly composes media items followed by the load-more trigger, which is the expected structure for HTMX-based infinite scrolling. The
load-more-trigger.htmlpartial properly uses{% if page_obj.has_next %}to conditionally render the trigger, preventing it from appearing on the last page.pyproject.toml (1)
18-18: python-dotenv dependency is correctly included and actively used.The dependency is imported in
src/config/settings.py(line 15) and used to load environment variables from a.envfile (line 21). The inclusion inpyproject.tomlis appropriate.src/config/settings.py (2)
15-25: LGTM! Environment variable loading looks correct.The dotenv loading and PATH augmentation for NVM support are well-implemented. The
.envfile location atBASE_DIR.parent(project root) is a sensible default.
198-204: Configuration looks good, minor duplication noted.The NPM_BIN_PATH resolution logic is correct and provides good flexibility for different Node.js installation methods. The
NVM_NODE_PATHcheck duplicates the earlier one at line 24, but since settings files are typically loaded once, this has negligible performance impact.src/templates/partials/spinner.html (1)
1-18: Well-documented and flexible spinner component.The parameterization with sensible defaults is clean. The documentation block clearly explains all available options.
One observation: when
show_text=Truewithoutinline=True, the spinner<div>and text<span>are sibling elements without a wrapper, which may cause layout issues. Based on the usage inload-more-trigger.html, bothshow_text=Trueandinline=Trueare passed together, so this works correctly in practice.src/tests/core/test_views.py (1)
576-616: Comprehensive pagination tests.Good coverage of pagination behavior including edge cases like the second page having remaining items. The tests properly verify both
media_listlength andpage_objflags (has_next,has_previous).src/core/views.py (3)
9-9: Appropriate import for pagination.
100-107: Pagination implementation is correct.Using
get_page()rather thanpage()is the right choice—it handles out-of-range and invalid page numbers gracefully without raising exceptions. The page size of 20 is appropriate for a media listing.
235-242: Consistent pagination in search view.The pagination implementation mirrors the index view, maintaining consistency across the codebase.
src/templates/partials/load-more-trigger.html (1)
1-24: Clean HTMX infinite scroll implementation.The template correctly implements the "revealed" trigger pattern for lazy loading. State preservation via
hx-includeensures sorting, filtering, and search parameters are maintained across page loads. All referenced input selectors (view-mode-input,sort,type,status,score,review-from,review-to, andsearch) are properly defined in parent templates. The dual rendering path (grid<div>vs table<tr>) appropriately handles different view modes.
- Implemented lazy loading for media items on the media list page to enhance performance and user experience. - Created new partial templates: load-more-trigger.html and media-items-page.html to support lazy loading functionality.
c4782e9 to
a6ea969
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
pyproject.tomlsrc/config/settings.pysrc/core/urls.pysrc/core/views.pysrc/templates/media.htmlsrc/templates/partials/load-more-trigger.htmlsrc/templates/partials/media-items-page.htmlsrc/templates/partials/media-list.htmlsrc/templates/partials/spinner.htmlsrc/tests/core/test_views.pysrc/theme/static_src/src/styles.css
🚧 Files skipped from review as they are similar to previous changes (6)
- src/templates/media.html
- src/templates/partials/media-items-page.html
- pyproject.toml
- src/core/urls.py
- src/config/settings.py
- src/theme/static_src/src/styles.css
🧰 Additional context used
🧠 Learnings (1)
📚 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/load-more-trigger.htmlsrc/templates/partials/media-list.htmlsrc/templates/partials/spinner.html
🧬 Code graph analysis (2)
src/core/views.py (2)
src/core/models.py (1)
Media(108-226)src/tests/conftest.py (1)
media(20-31)
src/tests/core/test_views.py (1)
src/tests/conftest.py (2)
logged_in_client(65-68)media_factory(35-49)
🔇 Additional comments (15)
src/templates/partials/spinner.html (2)
1-11: LGTM!The documentation comment is comprehensive and clearly describes all parameters with their types, valid values, and defaults. This will help future maintainers understand the component's API.
12-18: LGTM!The template logic correctly implements conditional rendering with sensible defaults. The use of Django's i18n for the loading text and proper handling of optional parameters makes this component flexible and maintainable.
src/core/views.py (8)
9-9: LGTM!The Paginator import is correctly placed and necessary for the new pagination functionality.
66-73: LGTM!The helper function properly centralizes search logic with appropriate case-insensitive filtering across multiple fields. The use of
distinct()is correct for preventing duplicate results from the ManyToManycontributorsrelationship.
110-114: LGTM!The pagination implementation correctly uses Django's
Paginatorwith a reasonable page size of 20 items. Usingget_page()is the right choice as it handles invalid page numbers gracefully.
116-117: LGTM!The context correctly includes both
page_objfor pagination controls andmedia_listderived frompage_obj.object_listfor template compatibility. This maintains a clean separation of concerns.
185-217: LGTM!The
load_more_mediaview is well-implemented with proper authentication, state preservation (sorting, filters, search, view_mode), and consistent use of helper functions. The conditional queryset building based on search query is correct, and pagination is properly configured.
227-227: LGTM!Refactoring to use the centralized
_build_search_querysethelper improves code consistency and maintainability.
232-236: LGTM!The pagination implementation in
search_mediais consistent with theindexview, ensuring a uniform user experience across different views.
238-239: LGTM!The context updates are consistent with the
indexview, maintaining uniformity across the codebase.src/templates/partials/media-list.html (1)
7-7: LGTM!The switch to including
media-items-page.htmlis correct and consistent across both grid and list view modes. This partial serves as a container that composes the media items and the load-more trigger, supporting the new infinite scroll functionality.Also applies to: 25-25
src/templates/partials/load-more-trigger.html (2)
14-23: LGTM!The list view trigger implementation is consistent with the grid view, with appropriate HTML structure using table rows. The
colspan="8"correctly matches the number of table columns defined in the parent template.
11-11: LGTM!The spinner usage correctly leverages the parameterized spinner component with appropriate parameters for the loading state. The explicit
textparameter provides clear context for the user.Also applies to: 20-20
src/tests/core/test_views.py (2)
576-616: LGTM!The
TestPaginationBehaviorclass provides comprehensive coverage of pagination functionality across index and search views. The tests correctly verify page size limits, navigation state, and pagination metadata.
618-720: LGTM!The
TestLoadMoreMediaViewclass provides excellent coverage of the newload_more_mediaview with 9 well-structured tests. The tests verify authentication, template rendering, pagination behavior, state preservation (sorting, filters, search, view_mode), and edge cases. This comprehensive test suite ensures the infinite scroll feature works correctly.
| <div id="load-more-trigger" | ||
| hx-get="{% url 'load_more_media' %}?page={{ page_obj.next_page_number }}" | ||
| hx-trigger="revealed" | ||
| hx-swap="outerHTML" | ||
| hx-include="#view-mode-input, #sort, #type, #status, #score, #review-from, #review-to, [name='search']" | ||
| class="col-span-full flex justify-center py-4"> | ||
| {% include "partials/spinner.html" with size="lg" show_text=True text=_("Loading more...") inline=True %} | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for filter input definitions in templates
echo "=== Checking for filter input IDs in templates ==="
rg -n "id=\"(view-mode-input|sort|type|status|score|review-from|review-to|contributor)\"" src/templates/
echo -e "\n=== Checking name attributes for filters ==="
rg -n "name=\"(contributor|type|status|score|review_from|review_to|view_mode|sort|search)\"" src/templates/
echo -e "\n=== Checking filter extraction in views.py ==="
rg -n "request.GET.get\(" src/core/views.py | head -20
echo -e "\n=== Looking for filter variable assignments ==="
rg -n "filters\[.*\]|request.GET.get" src/core/views.py | head -30Repository: PascalRepond/datakult
Length of output: 3928
Add contributor filter to hx-include to preserve filter state on infinite scroll.
The HTMX configuration correctly implements infinite scroll with hx-trigger="revealed" and hx-swap="outerHTML", and most filter state is properly preserved. However, the hx-include attribute is missing the contributor filter. When users apply a contributor filter and scroll to load more items, the contributor selection will not be sent in subsequent requests, causing the pagination to ignore that filter.
Add #contributor to the hx-include list:
Updated hx-include
hx-include="#view-mode-input, #sort, #type, #status, #score, #review-from, #review-to, #contributor, [name='search']"
🤖 Prompt for AI Agents
In src/templates/partials/load-more-trigger.html around lines 5 to 12, the
hx-include list omits the contributor filter so subsequent infinite-scroll
requests lose that filter; update the hx-include attribute to include
#contributor among the selectors (i.e., add ", #contributor" into the existing
comma-separated list) so the contributor selection is sent with each hx-get
request.
| {% load i18n %} | ||
| <div id="media-list"> | ||
| {% if media_list %} | ||
| {% if page_obj %} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the media-items-page.html file
find . -name "media-items-page.html" -o -name "media-items*" | head -20Repository: PascalRepond/datakult
Length of output: 153
🏁 Script executed:
# Check the media-list.html file to see the full context
cat -n src/templates/partials/media-list.htmlRepository: PascalRepond/datakult
Length of output: 1567
🏁 Script executed:
# Search for empty state handling patterns in template files
rg "No media|empty|if.*object_list|if.*count" src/templates/partials/ -A 2 -B 2Repository: PascalRepond/datakult
Length of output: 914
🏁 Script executed:
# Check the media-items-page.html file
cat -n src/templates/partials/media-items-page.htmlRepository: PascalRepond/datakult
Length of output: 279
🏁 Script executed:
# Also check media-items.html for reference
cat -n src/templates/partials/media-items.htmlRepository: PascalRepond/datakult
Length of output: 5046
Check the pagination condition to properly handle empty states.
The condition changed from {% if media_list %} to {% if page_obj %}, but this introduces a bug. Django's pagination object (page_obj) is always truthy even when the result set is empty. Since media-items.html has no empty state handling and only loops through media_list, an empty query will render an empty grid or table with no visual feedback to the user. The "No media found" message (lines 30-34) won't display.
Change the condition to {% if page_obj.object_list %} to properly show the empty state message when there are no results.
🤖 Prompt for AI Agents
In src/templates/partials/media-list.html around line 3, the template currently
checks {% if page_obj %} which is always truthy even when there are no results;
change the condition to check the actual list by using {% if
page_obj.object_list %} so the empty-state block (the "No media found" message
at lines ~30-34) will render when the query returns no items.