Add HTMX-driven admin tabs with lazy loading support#264
Add HTMX-driven admin tabs with lazy loading support#264
Conversation
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
There was a problem hiding this comment.
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. |
| # Tab not found - return empty content | ||
| return TemplateResponse(request, "touchtechnology/admin/_htmx_tab_empty.html", context) |
There was a problem hiding this comment.
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.
| hx-get="?_htmx_tab={{ name }}" | ||
| hx-target="#{{ name }}-tab" | ||
| hx-swap="innerHTML" |
There was a problem hiding this comment.
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.
| hx-get="?_htmx_tab={{ name }}" | |
| hx-target="#{{ name }}-tab" | |
| hx-swap="innerHTML" |
| {% 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"> |
There was a problem hiding this comment.
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.
| <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"> |
| {% load static %} | ||
| <script src="{% static 'django_htmx/htmx.min.js' %}"></script> |
There was a problem hiding this comment.
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.
| {% 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 %} |
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_TABSsetting to control the new HTMX tab mode (defaults toFalsefor backward compatibility)Lazy Settings Infrastructure:
LazySettingclass extendingSimpleLazyObjectwith numeric coercion support (__int__,__float__)S()helper function to return lazy settings objects for deferred evaluationTemplate Updates (
edit.html):htmx_admin_tabscontext variablehx-get,hx-trigger="load delay:100ms", andhx-swap="innerHTML"attributes for dynamic loadingBackend Support (
touchtechnology/common/sites.py):_is_htmx_request()method to detect HTMX requests viaHX-Requestheader_render_htmx_tab_content()method to render partial HTML for specific tab panesgeneric_edit()to intercept_htmx_tabquery parameter and return partial content when conditions are metContext Processor: Added
htmx_admin_tabs()context processor to expose the feature flag to templatesMiddleware: Added
django_htmx.middleware.HtmxMiddlewareto support HTMX request detectionPartial Templates:
_htmx_tab_related.html: Renders related object list for a specific tab_htmx_tab_empty.html: Fallback for non-existent or empty tabsHTMX Script: Added
django_htmx/htmx.min.jsto base templateImplementation Details
afterSwapevent handler re-initializes form plugins (iCheck, Select2) on dynamically loaded contentHX-Requestheader to prevent accidental partial page rendersTesting
https://claude.ai/code/session_014S55HPGYT3wpD9nHv7wV2z