Skip to content

Add HTMX-driven admin tabs with lazy loading support#264

Open
goodtune wants to merge 2 commits intomainfrom
claude/htmx-admin-tabs-migration-i0mNV
Open

Add HTMX-driven admin tabs with lazy loading support#264
goodtune wants to merge 2 commits intomainfrom
claude/htmx-admin-tabs-migration-i0mNV

Conversation

@goodtune
Copy link
Owner

@goodtune goodtune commented Feb 9, 2026

Summary

This PR introduces an optional HTMX-based admin interface for tabbed edit pages, enabling dynamic lazy-loading of related object tabs while maintaining full backward compatibility with the traditional tab-based interface.

Key Changes

  • Feature Flag: Added TOUCHTECHNOLOGY_HTMX_ADMIN_TABS setting to control the new HTMX tab mode (defaults to False for backward compatibility)

  • Lazy Settings Infrastructure:

    • Introduced LazySetting class extending SimpleLazyObject with numeric coercion support (__int__, __float__)
    • Updated S() helper function to return lazy settings objects for deferred evaluation
  • Template Updates (edit.html):

    • Conditional rendering based on htmx_admin_tabs context variable
    • In HTMX mode: form is scoped inside the active tab pane instead of wrapping all tabs
    • Related tab panes include hx-get, hx-trigger="load delay:100ms", and hx-swap="innerHTML" attributes for dynamic loading
    • Added HTMX-specific CSS and JavaScript for plugin re-initialization after content swaps (iCheck, Select2)
  • Backend Support (touchtechnology/common/sites.py):

    • Added _is_htmx_request() method to detect HTMX requests via HX-Request header
    • Added _render_htmx_tab_content() method to render partial HTML for specific tab panes
    • Modified generic_edit() to intercept _htmx_tab query parameter and return partial content when conditions are met
  • Context Processor: Added htmx_admin_tabs() context processor to expose the feature flag to templates

  • Middleware: Added django_htmx.middleware.HtmxMiddleware to support HTMX request detection

  • Partial Templates:

    • _htmx_tab_related.html: Renders related object list for a specific tab
    • _htmx_tab_empty.html: Fallback for non-existent or empty tabs
  • HTMX Script: Added django_htmx/htmx.min.js to base template

Implementation Details

  • Backward Compatible: Traditional mode remains the default; HTMX mode is opt-in via settings
  • Lazy Loading: Related tabs are pre-loaded with a 100ms delay after page load, improving perceived performance
  • Plugin Re-initialization: HTMX afterSwap event handler re-initializes form plugins (iCheck, Select2) on dynamically loaded content
  • Request Detection: HTMX tab requests require both the feature flag enabled AND the HX-Request header to prevent accidental partial page renders
  • Comprehensive Testing: Includes unit tests for lazy settings, context processors, template rendering, and view behavior; plus end-to-end tests for both traditional and HTMX modes

Testing

  • Unit tests cover lazy settings evaluation, feature flag behavior, and template conditional rendering
  • Integration tests verify HTMX tab content loading for competition and season admin views
  • E2E tests validate both traditional and HTMX tab modes with real browser interactions

https://claude.ai/code/session_014S55HPGYT3wpD9nHv7wV2z

Add HTMX-driven dynamic tab loading as an alternative to the existing
Bootstrap tab-based admin interface, controlled by the
TOUCHTECHNOLOGY_HTMX_ADMIN_TABS feature flag (default: False).

When enabled:
- Each tab pane loads its content dynamically via HTMX hx-get requests
- The primary edit form is wrapped in its own <form> tag inside its tab
  pane, with Save redirecting to the parent page
- Related object tabs pre-load in background via hx-trigger="load delay:100ms"
- jQuery plugins (iCheck, Select2) are re-initialized after HTMX swaps
- htmx.min.js is conditionally loaded in the admin base template

Changes:
- touchtechnology/common/default_settings.py: Add HTMX_ADMIN_TABS flag
- touchtechnology/common/context_processors.py: Expose flag to templates
- touchtechnology/common/sites.py: Add _render_htmx_tab_content() and
  _is_htmx_request() to Application; modify generic_edit() to handle
  HTMX tab content requests via _htmx_tab query parameter
- touchtechnology/admin/templates: Modify edit.html and base.html for
  conditional HTMX rendering; add partial templates for tab content
- tests/vitriolic/settings.py: Add HtmxMiddleware and context processor

Tests:
- 11 unit tests for feature flag, template rendering, and view behavior
- 10 competition-specific unit tests for HTMX tab content loading
- E2E Playwright tests covering both traditional and HTMX modes

https://claude.ai/code/session_014S55HPGYT3wpD9nHv7wV2z
- Convert S() and A() helpers to return LazySetting (SimpleLazyObject
  subclass with __int__/__float__ support) so settings are evaluated
  at access time rather than import time
- Context processor and sites.py read HTMX flag from django.conf.settings
  directly for full override_settings compatibility
- Always load HTMX script in base template (remove conditional)
- Refactor admin and competition unit tests to django-test-plus style
  with override_settings instead of @patch on module-level variables
- Refactor E2E tests to use reverse() + urljoin() instead of hard-coded
  URL paths
- Add tests for LazySetting lazy evaluation and type coercion

https://claude.ai/code/session_014S55HPGYT3wpD9nHv7wV2z
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an opt-in, HTMX-driven variant of the existing admin “tabbed edit” UI to lazily load related-object tabs, while keeping the legacy (non-HTMX) behavior as the default. It also introduces lazy evaluation for TouchTechnology settings to defer settings access until needed.

Changes:

  • Add feature-flagged HTMX tab loading support to generic_edit() (partial rendering for a requested tab via _htmx_tab + HTMX header detection).
  • Update admin edit templates to conditionally render either legacy Bootstrap tabs or HTMX-enabled lazy-loading tab panes.
  • Add supporting infrastructure: lazy settings wrapper (LazySetting), context processor, middleware wiring in the test project, plus unit/integration/e2e tests.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tournamentcontrol/competition/tests/test_htmx_admin_tabs.py Integration tests for HTMX tab partial responses on competition/season admin edit views.
touchtechnology/common/sites.py Implements HTMX request detection and partial tab rendering in generic_edit().
touchtechnology/common/default_settings.py Introduces LazySetting and makes S()/A() return lazily-evaluated settings.
touchtechnology/common/context_processors.py Exposes TOUCHTECHNOLOGY_HTMX_ADMIN_TABS to templates as htmx_admin_tabs.
touchtechnology/admin/tests/test_htmx_tabs.py Unit tests for lazy settings, context processor, and conditional template behavior.
touchtechnology/admin/templates/touchtechnology/admin/edit.html Conditional legacy vs HTMX tab rendering; adds lazy-loading attributes and plugin re-init hook.
touchtechnology/admin/templates/touchtechnology/admin/base.html Loads HTMX script resource.
touchtechnology/admin/templates/touchtechnology/admin/_htmx_tab_related.html Partial template for HTMX-loaded related-tab content.
touchtechnology/admin/templates/touchtechnology/admin/_htmx_tab_empty.html Partial template for missing/empty tab content.
tests/vitriolic/settings.py Enables django_htmx middleware + adds the new context processor for tests.
tests/e2e/test_htmx_admin_tabs.py Playwright E2E coverage for both legacy and HTMX modes.

Comment on lines +435 to +436
# Tab not found - return empty content
return TemplateResponse(request, "touchtechnology/admin/_htmx_tab_empty.html", context)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

In the tab-not-found branch, this returns a bare TemplateResponse instead of using Application.render(). That skips the normal response setup (e.g., adding application to context, setting current_app, and applying patch_cache_control(private=True) for authenticated users). Prefer returning via self.render(...) for consistency with the successful tab render path.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +36
hx-get="?_htmx_tab={{ name }}"
hx-target="#{{ name }}-tab"
hx-swap="innerHTML"
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

In HTMX mode the related tab link has hx-get and the tab pane also has its own hx-get + hx-trigger="load ...". That will cause duplicate requests (one on page load, then another when the user clicks the tab link). Consider removing hx-get from the link (let Bootstrap toggle visibility and let the pane preload), or removing the pane preload attributes and only load on click.

Suggested change
hx-get="?_htmx_tab={{ name }}"
hx-target="#{{ name }}-tab"
hx-swap="innerHTML"

Copilot uses AI. Check for mistakes.
{% block tab-panes %}
<div class="tab-pane active" id="{{ model|type|slugify }}-tab">
{% if htmx_admin_tabs %}
<form class="form-horizontal" action="" method="post" enctype="multipart/form-data">
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

In HTMX mode the inner <form> tag hardcodes class="form-horizontal" and no longer uses the {% block form-class %} hook used in traditional mode. If any downstream templates override form-class, they won’t apply when HTMX mode is enabled. Mirror the same block usage (and any other form tag customizations) in the HTMX form tag to preserve backward compatibility.

Suggested change
<form class="form-horizontal" action="" method="post" enctype="multipart/form-data">
<form class="{% block form-class %}form-horizontal{% endblock %}" action="" method="post" enctype="multipart/form-data">

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +186
{% load static %}
<script src="{% static 'django_htmx/htmx.min.js' %}"></script>
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The HTMX script is included unconditionally, even when the feature flag is off. Since this PR introduces HTMX tabs as an opt-in feature, consider only including htmx.min.js when htmx_admin_tabs is enabled (using the new context processor) to avoid extra JS payload on legacy mode pages.

Suggested change
{% load static %}
<script src="{% static 'django_htmx/htmx.min.js' %}"></script>
{% load static %}
{% if htmx_admin_tabs %}
<script src="{% static 'django_htmx/htmx.min.js' %}"></script>
{% endif %}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants