Skip to content

feat: add lazy loading for media list#29

Merged
PascalRepond merged 1 commit intostagingfrom
rep-dev
Dec 31, 2025
Merged

feat: add lazy loading for media list#29
PascalRepond merged 1 commit intostagingfrom
rep-dev

Conversation

@PascalRepond
Copy link
Copy Markdown
Owner

  • 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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Dependency Management
pyproject.toml
Added python-dotenv>=1.2.1.
Environment Configuration
src/config/settings.py
Load .env via load_dotenv; prepend NVM_NODE_PATH to PATH when set; introduce NPM_BIN_PATH resolution (NVM-derived or env/default).
Core Routing
src/core/urls.py
Added route path("load-more/", views.load_more_media, name="load_more_media").
View Logic
src/core/views.py
Added _build_search_queryset; paginate index and search_media (20/page); added load_more_media HTMX view that applies sorting, filters, search and returns a page partial.
Template Composition
src/templates/media.html
Pass explicit spinner parameters (id="spinner" size="xl" htmx_indicator=True) to spinner include.
Template Partials
src/templates/partials/media-items-page.html, src/templates/partials/load-more-trigger.html, src/templates/partials/media-list.html, src/templates/partials/spinner.html
Added media-items-page.html (page fragment); added load-more-trigger.html (HTMX reveal trigger with preserved state inputs); refactored media-list.html to use page fragment; parameterized spinner.html (id, size, color, htmx_indicator, show_text, inline) with i18n.
Static Assets
src/theme/static_src/src/styles.css
Added exclusion pattern to Tailwind @source to ignore node_modules.
Test Coverage
src/tests/core/test_views.py
Added TestPaginationBehavior and TestLoadMoreMediaView covering pagination, load-more auth, partial rendering, paging, sorting/filters preservation, and view_mode propagation.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add lazy loading for media list' directly and clearly reflects the main objective of the changeset: implementing lazy loading functionality for media items.
Description check ✅ Passed The description accurately relates to the changeset by highlighting the two key aspects: lazy loading implementation and the creation of new partial templates (load-more-trigger.html and media-items-page.html).
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 by load_more_media view, both media_list (set to page_obj.object_list) and page_obj are always provided together. Consider simplifying to {% if page_obj %} since page_obj is 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=999 or page=0). Django's Paginator.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

📥 Commits

Reviewing files that changed from the base of the PR and between 0e25398 and c4782e9.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • pyproject.toml
  • src/config/settings.py
  • src/core/urls.py
  • src/core/views.py
  • src/templates/media.html
  • src/templates/partials/load-more-trigger.html
  • src/templates/partials/media-items-page.html
  • src/templates/partials/media-list.html
  • src/templates/partials/spinner.html
  • src/tests/core/test_views.py
  • src/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.html
  • src/templates/partials/load-more-trigger.html
  • src/templates/media.html
  • src/templates/partials/media-list.html
  • src/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_modules from 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_media view 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 the hx-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.html maintains 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 to media-items-page.html correctly includes both media-items.html and load-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.html partial 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 .env file (line 21). The inclusion in pyproject.toml is 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 .env file location at BASE_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_PATH check 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=True without inline=True, the spinner <div> and text <span> are sibling elements without a wrapper, which may cause layout issues. Based on the usage in load-more-trigger.html, both show_text=True and inline=True are 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_list length and page_obj flags (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 than page() 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-include ensures 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, and search) 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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4782e9 and a6ea969.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • pyproject.toml
  • src/config/settings.py
  • src/core/urls.py
  • src/core/views.py
  • src/templates/media.html
  • src/templates/partials/load-more-trigger.html
  • src/templates/partials/media-items-page.html
  • src/templates/partials/media-list.html
  • src/templates/partials/spinner.html
  • src/tests/core/test_views.py
  • src/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.html
  • src/templates/partials/media-list.html
  • src/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 ManyToMany contributors relationship.


110-114: LGTM!

The pagination implementation correctly uses Django's Paginator with a reasonable page size of 20 items. Using get_page() is the right choice as it handles invalid page numbers gracefully.


116-117: LGTM!

The context correctly includes both page_obj for pagination controls and media_list derived from page_obj.object_list for template compatibility. This maintains a clean separation of concerns.


185-217: LGTM!

The load_more_media view 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_queryset helper improves code consistency and maintainability.


232-236: LGTM!

The pagination implementation in search_media is consistent with the index view, ensuring a uniform user experience across different views.


238-239: LGTM!

The context updates are consistent with the index view, maintaining uniformity across the codebase.

src/templates/partials/media-list.html (1)

7-7: LGTM!

The switch to including media-items-page.html is 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 text parameter provides clear context for the user.

Also applies to: 20-20

src/tests/core/test_views.py (2)

576-616: LGTM!

The TestPaginationBehavior class 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 TestLoadMoreMediaView class provides excellent coverage of the new load_more_media view 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.

Comment on lines +5 to +12
<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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -30

Repository: 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 %}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the media-items-page.html file
find . -name "media-items-page.html" -o -name "media-items*" | head -20

Repository: 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.html

Repository: 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 2

Repository: PascalRepond/datakult

Length of output: 914


🏁 Script executed:

# Check the media-items-page.html file
cat -n src/templates/partials/media-items-page.html

Repository: PascalRepond/datakult

Length of output: 279


🏁 Script executed:

# Also check media-items.html for reference
cat -n src/templates/partials/media-items.html

Repository: 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.

@PascalRepond PascalRepond merged commit 0b0747a into staging Dec 31, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant