Plan for Modular Template Packs
Objective
Enable PowerCRUD to support multiple frontend presentation layers (DaisyUI, Tailwind-base, Bootstrap, Bulma, etc.) by introducing a modular template-pack architecture. The goal is to decouple PowerCRUD’s functional logic from its visual layer so that projects can adopt any CSS framework without rewriting PowerCRUD internals.
Risk and Worth
It is a substantial refactor with real complexity and risk. The payoff depends on whether:
- PowerCRUD aspires to be a widely adopted enabling framework
- Third-party adoption and community contribution is a goal
- Long-term maintainability and clean architecture matter
If the intent for PowerCRUD is:
- general-purpose OSS library with real extensibility value
- showcase engineering quality for credibility and adoption
- enable plug-and-play customization
Then it is worth the effort.
The key to making it safe is the sequence: contract → extract JS → extract CSS → refactor DaisyUI → add tests → only then build new packs
Reason for Template Packs
The current implementation embeds DaisyUI-specific HTML, CSS, and JavaScript directly inside core templates (e.g., object_list.html). This creates tight coupling that prevents:
- switching UI frameworks
- supporting different styling variants
- enabling community-authored packs
- controlling regression risk during visual changes
- maintaining clean separation between UX details and core behaviors
A template-pack system allows PowerCRUD to:
- standardize rendering through a stable contract of template names, partials, block definitions, and context variables
- isolate pack-specific CSS/JS
- minimize maintenance surface area
- offer future extensibility with lower risk
Framework Approach
At a high level, the work breaks down into:
- defining a clear contract for templates and JavaScript
- splitting shared “core” behavior from per-template-pack behavior
- turning the existing DaisyUI implementation into the first pack
- adding a clean way to select packs and test them
The list below is the concrete checklist we will actually follow.
High-Level Task List
-
Define template + JavaScript contract
- 1.1 Inventory existing templates (list, form, modal, partials), context variables, HTMX snippets, and how
partialdef/ inline partials are used today. - 1.2 Critically review the current template architecture: inline
partialdefvs separate partial files with{% include %}, how filters and other sub-components are structured, and how easy this is for future template-pack authors to understand. - 1.3 Evaluate
HtmxMixin.get_framework_stylesand consider strategy for template-pack modularisation. - 1.4 Decide on the standard structure for template packs: which templates, blocks, and partials must exist, where they live (inline vs separate files), and naming conventions.
- 1.5 Specify the JavaScript API: core init (
initPowercrud(fragment)), pack init (initPowercrudPack(fragment)), and any custom events/hooks. - 1.6 Decide template-pack packaging and discovery strategy (where packs live, naming conventions, how core locates templates/styles).
- 1.1 Inventory existing templates (list, form, modal, partials), context variables, HTMX snippets, and how
-
Implement core vs template-pack JavaScript split
- 2.1 Move the existing inline
<script>fromobject_list.htmlintopowercrud/static/powercrud/powercrud.jsand exposewindow.initPowercrud(fragment). - 2.2 Update the real base template to load
powercrud.jsonce using{% static 'powercrud/powercrud.js' %}. - 2.3 Remove inline
<script>tags from swapped fragments and addhx-on="htmx:load: initPowercrud(this)"(or equivalent) on the fragment root. - 2.4 Extract DaisyUI-specific code from
powercrud.jsintopowercrud_daisyui/static/powercrud_daisyui/daisyui.jsaswindow.initPowercrudPack(fragment). - 2.5 Decide whether the fragment calls both initializers (
hx-on="htmx:load: initPowercrud(this); initPowercrudPack(this)") or core JS callsinitPowercrudPackif present.
- 2.1 Move the existing inline
-
Turn DaisyUI into the first template pack
- 3.1 Move DaisyUI templates into a
powercrud_daisyuitemplate namespace. - 3.2 Update paths and
{% extends %}/{% include %}usage to go through the template-pack contract. - 3.3 Remove DaisyUI-specific markup from core templates; keep only framework-neutral structure and blocks.
- 3.4 Verify the DaisyUI pack conforms to the contract (templates, blocks, context, JS hooks).
- 3.5 implement refactor of get_framework_styles in line with strategy from 1.3 above.
- 3.1 Move DaisyUI templates into a
-
Add template-pack selection and discovery
- 4.1 Introduce a
POWERCRUD_TEMPLATE_PACKsetting, with DaisyUI as the default. - 4.2 Implement loader helpers that resolve template names and
pack_js_pathbased on the selected pack. - 4.3 Load the active pack JS explicitly in the base template:
<script src="{% static 'powercrud/powercrud.js' %}"></script>
<script src="{% static pack_js_path %}"></script>
- 4.1 Introduce a
-
Build and extend tests (including Playwright)
- 5.1 Add/extend unit tests to check that required templates and blocks exist for each pack.
- 5.2 Add a small Playwright smoke suite per pack (at least CRUD list + form) to verify the JS lifecycle and visual wiring.
- 5.3 Ensure CI runs the core test suite and a minimal Playwright matrix for the default pack.
- 5.4 Implement a
validate_template_pack()helper (and optional management command) that checks a pack for contract compliance (required templates/partials,PACK_STYLESpresence, etc.), and wire it into the test suite. - 5.5 Define and document guidance for template-pack authors on testing: which central PowerCRUD tests they should run to validate their pack, when to use
validate_template_pack(), and when they should add their own pack-specific tests (e.g. for custom JS or UX behavior).
-
Documentation and cookbook
- 6.1 Update the PowerCRUD docs to explain template packs, the contract, and how to switch packs.
- 6.2 Write a short “create your own template pack” cookbook, including the JS structure (
powercrud.js+ per-pack JS) and Playwright test patterns.
-
Dogfood with a Bootstrap 5 template pack
- 7.1 Implement a minimal Bootstrap 5 pack using the same contract as DaisyUI (templates + JS file) to prove the design works beyond DaisyUI.
- 7.2 Fix any issues the Bootstrap pack reveals in the contract (missing hooks, leaky DaisyUI assumptions).
- 7.3 Evolve the Bootstrap 5 pack from “minimal demo” to a production-ready alternative: cover the full CRUD surface (lists, forms, filters, modals, bulk actions, inline editing) with Bootstrap-styled templates.
- 7.4 Add Bootstrap 5 to tests and docs as a concrete, production-quality example of a second pack, and use it in the sample app as a real dogfooding target.
- 7.5 Checkpoint: after completing 7.2, decide whether 7.3–7.4 happen in this refactor phase or are deferred to a follow-up phase (possibly after documentation is in place).
- 7.6 Update or amend the documentation from step 6 based on what we learn building and using the Bootstrap 5 pack (clarify the contract, add gotchas, extend the cookbook with Bootstrap-specific examples).
Template-Pack Contract Working Notes
This section is a living scratchpad while we work through the tasks above. As we complete 1.x, 2.x, 5.x, and 7.x, we capture the concrete decisions here so we can later fold them into the official docs and cookbook.
Templates & blocks (per Task 1.1)
-
DaisyUI core views:
-
powercrud/daisyUI/object_list.html- Partials:
pcrud_contentbulk_selection_statusfiltered_resultspagination
- Partials:
-
powercrud/daisyUI/object_form.html- Partials:
pcrud_contentconflict_detectednormal_content
- Partials:
-
powercrud/daisyUI/object_detail.html- Partials:
pcrud_content
- Partials:
-
powercrud/daisyUI/object_confirm_delete.html- Partials:
pcrud_contentconflict_detectednormal_content
- Partials:
-
-
Shared partial templates:
-
powercrud/daisyUI/partial/list.html- Partials:
inline_row_displayinline_row_form
- Partials:
-
powercrud/daisyUI/partial/detail.html- Behavior:
- detail layout partial (no
partialdefdefined inside; used via{% partial %}/{% include %})
- detail layout partial (no
- Behavior:
-
powercrud/daisyUI/partial/bulk_edit_errors.html- Partials:
bulk_edit_errorbulk_edit_conflict
- Partials:
-
powercrud/daisyUI/crispy_partials.html- Partials:
load_tagscrispy_form
- Partials:
-
powercrud/daisyUI/bulk_edit_form.html- Partials:
full_formasync_queue_success
- Partials:
-
Framework styles and get_framework_styles (task 1.3)
-
How it works today
HtmxMixin.get_framework_styles()(src/powercrud/mixins/htmx_mixin.py) returns a dict keyed by framework name (currently only'daisyUI').- For
'daisyUI'it provides:base: base CSS class for buttons (e.g."btn ").filter_attrs: widgetattrsfor different field types (text,select,multiselect,date,number,time,default) used byFilteringMixinwhen auto-building filter forms.actions: mapping of action names ("View","Edit","Delete") to button classes.extra_default: default class for “extra” buttons.modal_attrs: DaisyUI-specific modal trigger markup (onclick="...showModal()") built usingget_modal_id().
- Call sites:
FilteringMixin.get_filterset()(src/powercrud/mixins/filtering_mixin.py) pullsfilter_attrsto choose widget attributes per Django field type.action_linksandextra_buttonsinsrc/powercrud/templatetags/powercrud.pyusebase,actions,extra_default, andmodal_attrsto build list-view buttons.- Tests provide small
get_framework_styles()stubs (e.g. intest_form_filter_template_mixins.py,test_templatetags_powercrud.py).
-
Issues / limitations
- Framework-specific and centralised: the DaisyUI configuration lives in a core mixin; adding another pack means overriding
get_framework_stylesor forking the mixin. - Mixed concerns: one dict carries filter widget attrs, button styling, and modal JS behaviour; these evolve at different times and for different reasons.
- JS embedded in styles:
modal_attrshardcodes DaisyUI’sshowModal()JS in Python, which is awkward when packs should own their own JS. - Tight coupling to global settings: it assumes a single
POWERCRUD_CSS_FRAMEWORKrather than per-pack style modules discoverable by name. - Not obviously “pack-local”: there is no clear place for a pack (e.g.
powercrud_bootstrap5) to ship its own styles; everything routes through the core mixin.
- Framework-specific and centralised: the DaisyUI configuration lives in a core mixin; adding another pack means overriding
-
Refactoring direction
- Move style data into pack modules: each template pack will provide a small Python module (e.g.
powercrud_daisyui.styles) exporting aPACK_STYLESdict or similar structure. Core will become a consumer:HtmxMixin.get_framework_styles()will delegate to a helper such asget_pack_styles(active_pack_name). - Split concerns inside style data:
filter_attrsused byFilteringMixinto build filter widgets.buttonsfor base button class + per-action classes + extra button default.modaldescribing neutral modal-trigger attributes (e.g.data-pc-open-modal) rather than raw JS.
- Let pack JS handle modal behaviour: packs will listen for neutral attributes (e.g.
data-pc-open-modal) in their own JS (powercrud_daisyui/daisyui.js,powercrud_bootstrap5/bs5.js) and translate that to framework-specific calls (such as DaisyUI’sshowModal()). - Align with the future widget registry: over time, extend
PACK_STYLESto include awidgetssection, as sketched in20251114_widget_registry.md, so packs can define per-field/per-context widget templates and classes.filter_attrsthen becomes part of that wider registry rather than a one-off. - Keep the API simple for pack authors: they provide a Django app with templates, static assets, and a
styles.pymodule; PowerCRUD will resolve the active pack name (futurePOWERCRUD_TEMPLATE_PACKsetting) and importpack_name -> styles_modulevia a small registry or naming convention.
- Move style data into pack modules: each template pack will provide a small Python module (e.g.
Template structure principles
-
One orchestrator template per view
- Keep a single main template for each CRUD view type (
object_list.html,object_form.html,object_detail.html,object_confirm_delete.html) that defines the overall layout and calls partials. - View-specific logic that only makes sense in that context can live as inline
partialdefblocks inside that file.
- Keep a single main template for each CRUD view type (
-
Extract large or reusable partials into dedicated files
- When a
partialdefgrows large (filters, table rendering, pagination, inline rows, bulk edit forms) or is a natural override point for packs, move its implementation into a separatepartial/*.htmlfile and have thepartialdefinclude it. - Example pattern:
{% partialdef filtered_results %}{% include 'powercrud/daisyUI/partial/list.html' %}{% endpartialdef %}
- When a
-
Prefer separable partials for pack authors
- Pieces like filter UI, table body, pagination, bulk actions, and inline row templates should each have their own named partial so a pack can override them independently.
- The goal is that a pack author can focus on a small set of clearly named templates/partials instead of editing a single huge file.
-
Base template is owned by the project
- PowerCRUD does not provide a full HTML base shell; every project must define its own site base (navigation,
<head>, assets) and setbase_template_pathon each CRUD view to point at it. - Template packs are responsible only for the inner CRUD templates/partials; they assume the project base template already loads HTMX, JS, and CSS as needed.
- PowerCRUD does not provide a full HTML base shell; every project must define its own site base (navigation,
Template/partial placement plan (Task 1.4)
-
This section records, per template, which
partialdefblocks stay inline vs move into dedicatedpartial/*.htmlfiles so packs share a consistent override surface. -
DaisyUI core views:
-
powercrud/daisyUI/object_list.htmlpcrud_content– stays inline; orchestrator wrapper that wires header, filter controls, bulk-selection status, table, pagination, and the modal shell together.bulk_selection_status– move main markup to a dedicatedpartial/bulk_actions.htmlfile; keep this partial as a thin wrapper that{% include %}s it so packs can override bulk actions without copying the entire list template.filtered_results– stays inline as a thin wrapper that calls the list/table partial and thenpagination; no separate file needed.pagination– move pagination markup topartial/pagination.html; keep this partial as a thin wrapper include so packs can override pagination independently of the table.
-
powercrud/daisyUI/object_form.htmlpcrud_content– stays inline; view-level orchestrator for conflict vs normal form content.conflict_detected– stays inline; small view-specific message, not reused elsewhere.normal_content– stays inline; form layout is inherently view-specific and usually overridden at the whole-template level.
-
powercrud/daisyUI/object_detail.htmlpcrud_content– stays inline; simple wrapper around{% object_detail object view %}and layout chrome.
-
powercrud/daisyUI/object_confirm_delete.htmlpcrud_content– stays inline; orchestrator that chooses between conflict vs normal delete content.conflict_detected– stays inline; small, view-specific banner.normal_content– stays inline; delete-confirmation layout is view-specific and not reused.
-
-
Shared partial templates:
-
powercrud/daisyUI/partial/list.html- Main table markup + inline-edit wiring stay in this partial file; it is already the natural override point for list rows.
inline_row_display– stays in this file; tightly coupled to the main table markup and header structure.inline_row_form– stays in this file; shares structure and data attributes with the display row and uses the same table skeleton.
-
powercrud/daisyUI/partial/detail.html- Stays as a single simple partial; no internal
partialdefblocks, and the detail layout is already neatly isolated.
- Stays as a single simple partial; no internal
-
powercrud/daisyUI/partial/bulk_edit_errors.htmlbulk_edit_error– stays in this partial file; specific to bulk operations.bulk_edit_conflict– stays in this partial file; specific to bulk operation conflict state.
-
powercrud/daisyUI/crispy_partials.htmlload_tags– stays here; tiny and only used by crispy-aware forms.crispy_form– stays here; packs that use crispy can override this partial file as a unit.
-
powercrud/daisyUI/bulk_edit_form.htmlfull_form– stays inline; this template is itself the orchestrator for the bulk-edit modal, and there is no other caller.async_queue_success– stays inline; conceptually part of the bulk-edit modal flow and only used from this template.
-
Context variables
-
List / table (primarily from
object_list.html+partial/list.html):object_list,object_verbose_name,object_verbose_name_plural,headers,row.cells,row.actions,row.inline_url,row.inline_allowed,row.inline_blocked_reason,row.inline_blocked_label.- Pagination:
is_paginated,paginator,page_obj,page_size_options,default_page_size. - Selection / bulk edit:
enable_bulk_edit,selected_ids,selected_count,all_selected,some_selected,list_view_url,selection_key_suffix,keyBase. - Filtering/sorting:
filterset,filter_params,table_max_col_width,table_max_height,table_pixel_height_other_page_elements,table_classes,request.GET. - HTMX wiring:
use_htmx,use_modal,original_target,htmx_target,header_title. -
Forms / detail / delete:
-
Form views:
form,use_crispy,use_modal,update_view_url,create_view_url,list_view_url,object,object_verbose_name,conflict_detected,conflict_message,filter_params. - Bulk edit:
bulk_fields,field_info,selected_ids,selected_count,model_name_plural,enable_bulk_delete,task_name,message.
JavaScript lifecycle & hooks
-
Current state (before refactor):
- Large inline
<script>inobject_list.htmlhandles:- Tooltips via
tippy(...)andinitializeTooltips(). - Filter form behavior (
resetFilterForm,initializeFilterToggle,removeEmptyFields, preservation offilterExpandedstate vialocalStorage). - Bulk selection and bulk edit events (
toggleAllSelection,handleRowSelectionChange,clearSelectionOptimistic,updateBulkActionsCounter), usinghtmx.ajaxcalls to toggle selection and refresh#bulk-actions-container. - Global event listeners on
document.bodyforbulkEditSuccess,bulkEditQueued,refreshTable,inline-row-locked,inline-row-forbidden,inline-row-error, and severalhtmx:*events (htmx:beforeRequest,htmx:afterRequest,htmx:responseError,htmx:afterSwap,htmx:beforeSwap). - Inline editing helpers (row locking, width snapshots, focus management, notice banners, dependency endpoints, save/cancel spinners).
- Tooltips via
- Additional inline
<script>inbulk_edit_form.htmlcontrols toggling of bulk-edit field inputs and delete confirmation UI. -
Target state (for contract):
-
Core JS moved into
powercrud/static/powercrud/powercrud.jswith a single entrypointinitPowercrud(fragment)wired via HTMX lifecycle (htmx:load). - Per-pack JS (e.g. DaisyUI) moved into
powercrud_daisyui/static/powercrud_daisyui/daisyui.jsexposinginitPowercrudPack(fragment). - No inline
<script>blocks inside swapped templates; all behavior initialized through the core/pack initializers and HTMX events.
- Large inline
-
JS API and events (Task 1.5):
-
Core initializer:
initPowercrud(fragment)- Exposed on
windowbypowercrud.js. fragmentis eitherdocument(for full-page loads) or the root element of an HTMX swap (for partial updates).- May be called multiple times; it must be idempotent for a given fragment and guard any once-per-page wiring with an internal flag.
- Must not reset filter values or other user-entered state; it only attaches behaviour based on the current DOM and any stored state.
- Responsibilities:
- One-time global wiring on
document/document.body(HTMX event listeners, inline-edit helpers, bulk-selection helpers, form spinners). - Per-fragment wiring that searches inside
fragmentfor PowerCRUD hooks such as:data-powercrud-form="object"(form submit spinner / error handling).- Inline-editing hooks:
tr[data-inline-row="true"],data-inline-field,data-inline-save,data-inline-cancel,data-inline-dependent-field,data-inline-endpoint. - Bulk-selection hooks:
.row-select-checkbox,#select-all-checkbox, and the bulk-actions container that listens forbulkSelectionChanged.
- It should rely on data attributes and semantic IDs where possible so packs can restyle markup without changing behaviour.
- One-time global wiring on
- Exposed on
-
Pack initializer:
initPowercrudPack(fragment)- Exposed on
windowby each pack’s JS (e.g.powercrud_daisyui/daisyui.js). - Optional, but recommended for packs that need framework-specific behaviour.
- Called with the same
fragmentasinitPowercrud, after the core initializer has run. - Responsibilities are strictly framework-specific, for example:
- Implementing neutral modal triggers (e.g. listening for
data-pc-open-modalfrom templates and mapping that to DaisyUI’s.showModal()or Bootstrap’sModal.show()). - Attaching framework-specific tooltips or toasts in response to core events (e.g.
inline-row-error,bulkEditSuccess). - Any pack-only UX embellishments that do not change the semantics of core events or data attributes.
- Implementing neutral modal triggers (e.g. listening for
- Must be safe to call multiple times and should avoid re-registering global listeners without guards.
- Exposed on
-
Wiring strategy:
- Full-page loads: the real project base template includes
powercrud.js(and the active pack’s JS) and callsinitPowercrud(document)onDOMContentLoaded. If a pack JS file is present and definesinitPowercrudPack, it is called withdocumentimmediately after. - HTMX swaps: CRUD fragments use
htmx:loadto initialise JS.- Preferred:
powercrud.jsregisters a single globalhtmx:loadlistener and callsinitPowercrud(event.detail.elt)(andinitPowercrudPack(event.detail.elt)if present).initPowercrudmust be cheap and defensive so that whenevent.detail.eltis not a PowerCRUD fragment it quickly returns without doing work. - Alternative: templates attach
hx-on="htmx:load: initPowercrud(this); if (window.initPowercrudPack) { initPowercrudPack(this); }"on the fragment root to opt in explicitly. This keeps the same API but spreads the wiring logic into templates.
- Preferred:
- The contract for packs is that
initPowercrudalways runs first; packs should not assume they need to rewire or duplicate core behaviours.
- Full-page loads: the real project base template includes
-
Events and hooks:
- HTMX events that core JS listens to on
document.body:htmx:beforeRequest,htmx:afterRequest,htmx:afterSwap,htmx:beforeSwap,htmx:responseError.- These are used for inline-edit guards, table refresh behaviour, form spinners, and error handling.
- Custom DOM events fired on
document.bodyas part of the PowerCRUD lifecycle:bulkEditSuccess– indicates a bulk operation completed successfully; core currently closes the bulk-edit modal and clears selection, packs may also listen to show notifications.bulkEditQueued– indicates a bulk operation has been queued; packs can use this to show a “queued” message.refreshTable– tells the list view to refresh#filtered_resultsusing current filters and sort; packs should not override this behaviour but may listen to react visually.inline-row-locked,inline-row-forbidden– guard events raised when inline editing is blocked (e.g. by locks or permissions); detail payload includes at least row identifiers and messages.inline-row-error– indicates an inline save failed; detail payload includes arow_id(where possible) and message; core scrolls/focuses the failing row and shows an inline notice.bulkSelectionChanged– logical event that causes the bulk-actions container to re-evaluate visibility and counts (viahx-trigger="bulkSelectionChanged from:body"); any JS can trigger it viahtmx.trigger(document.body, 'bulkSelectionChanged', detail).
- HX-Trigger keys used by the server to coordinate with JS:
- Form-related:
formError,modalFormSuccess,refreshList,refreshUrl. - Bulk-related:
bulkEditSuccess,refreshTable(e.g. from async bulk operations), plus any pack-agnostic triggers added later. - Packs should treat these as high-level signals only and avoid depending on low-level HX header details.
- Form-related:
- HTMX events that core JS listens to on
-
Contract for packs consuming the JS API:
- Packs can assume
initPowercrudwires all behaviours tied to PowerCRUD’sdata-*attributes and does not depend on specific CSS classes. - Packs may define
initPowercrudPackto:- Implement modal opening/closing and other framework-specific UI concerns based on neutral attributes and events.
- Listen for the documented custom events (
bulkEditSuccess,inline-row-error, etc.) to show framework-specific notifications or visual feedback.
- Packs must not change the semantics of these events (e.g. reusing
bulkEditSuccessfor unrelated purposes), but they are free to add additional pack-specific events under their own names.
- Packs can assume
-
Testing expectations for packs
- (to be filled from 5.2–5.4 and 7.x)
Template-pack packaging & repository strategy (task 1.6 – initial thoughts)
-
Where packs live
- Each template pack is a Django app. The canonical naming convention is
powercrud_<packname>(e.g.powercrud_daisyui,powercrud_bootstrap5). - Core will treat the DaisyUI pack as a first-class app (e.g.
powercrud_daisyui) that ships in the same wheel aspowercrudand is added toINSTALLED_APPSalongside it. This is different from thesampleapp, which remains dev-only and is not included in the published package. - Additional “official” packs (e.g. a Bootstrap 5 pack) can also live in this repo as separate Django apps, or in their own repos as pip-installable packages, as long as they follow the same conventions below.
- The template-pack loader must not assume packs are co-located with core; as long as the app can be imported and is in
INSTALLED_APPS, it should be discoverable.
- Each template pack is a Django app. The canonical naming convention is
-
Pack app structure (per-pack layout)
- Every pack app provides, at minimum:
-
Templates under a stable namespace:
- Orchestrator templates at the root of the namespace:
powercrud_<packname>/templates/powercrud_<packname>/object_list.htmlpowercrud_<packname>/templates/powercrud_<packname>/object_form.htmlpowercrud_<packname>/templates/powercrud_<packname>/object_detail.htmlpowercrud_<packname>/templates/powercrud_<packname>/object_confirm_delete.html
- Shared partials grouped under a
partial/folder:powercrud_<packname>/templates/powercrud_<packname>/partial/list.htmlpowercrud_<packname>/templates/powercrud_<packname>/partial/detail.htmlpowercrud_<packname>/templates/powercrud_<packname>/partial/bulk_edit_errors.htmlpowercrud_<packname>/templates/powercrud_<packname>/partial/bulk_edit_form.html(optional wrapper for bulk-edit flows).powercrud_<packname>/templates/powercrud_<packname>/partial/crispy_partials.html(crispy helpers; conceptually a partial even if today’s DaisyUI path iscrispy_partials.htmlat the root).
-
Existing DaisyUI templates will be migrated toward this “orchestrators at root, shared pieces in
partial/” pattern; when we move files (e.g.crispy_partials.html), we will update the corresponding call sites in views/templates at the same time.
- Static assets in the usual Django layout: -
powercrud_<packname>/static/powercrud_<packname>/js/...(e.g.daisyui.jsimplementinginitPowercrudPack). -
powercrud_<packname>/static/powercrud_<packname>/css/...(if the pack ships its own CSS helpers).
- A styles module: -
powercrud_<packname>/styles.pyexportingPACK_STYLES, as described in the framework styles section (filter_attrs, button styles, modal metadata, and future widget/widget-registry definitions).- DaisyUI’s current templates live under
powercrud/daisyUI/...; the long-term plan (Task 3.x) is to migrate them into a properpowercrud_daisyuiapp using the structure above. Until then, core will support a legacy path for the built-in pack.
- DaisyUI’s current templates live under
- Orchestrator templates at the root of the namespace:
-
- Every pack app provides, at minimum:
-
Pack selection and discovery
-
Core will use a single setting to choose the active pack, e.g.:
-
POWERCRUD_TEMPLATE_PACK = "daisyui"(default) or"bootstrap5","mycorp_theme", etc.- By convention, a pack name maps to:
-
App label / Python package:
powercrud_<PACK>(e.g."daisyui" -> "powercrud_daisyui"). - Template prefix:
templates/powercrud_<PACK>/.... -
Styles module:
powercrud_<PACK>.stylesprovidingPACK_STYLES.- Core will provide small helpers (internal API) to resolve the active pack:
-
get_active_pack()→"daisyui". get_pack_app_label()→"powercrud_daisyui".get_pack_template_prefix()→"powercrud_daisyui".get_pack_styles()→ importspowercrud_daisyui.styles.PACK_STYLES.- If a configured pack cannot be imported or is not present in
INSTALLED_APPS, core should raise a clearImproperlyConfigurederror indicating thatPOWERCRUD_TEMPLATE_PACK='...'requires the corresponding app to be installed and added toINSTALLED_APPS.
- If a configured pack cannot be imported or is not present in
-
-
-
Central vs decentralised repos
- Centralised “official” packs (DaisyUI, Bootstrap 5) living in this repo simplify versioning, documentation, and CI. These packs are shipped in the same PyPI distribution as
powercrudbut remain separate Django apps. - Decentralised third-party packs (separate repos, pip-installable apps) follow the same conventions: they define a Django app with the expected templates, static assets, and
styles.py, and projects enable them viaINSTALLED_APPSandPOWERCRUD_TEMPLATE_PACK. - The loader logic deliberately does not care whether a pack lives in this repo or elsewhere; that distinction is about governance and maintenance, not the technical interface.
- Centralised “official” packs (DaisyUI, Bootstrap 5) living in this repo simplify versioning, documentation, and CI. These packs are shipped in the same PyPI distribution as
-
Compatibility and future widget work
- In the short term (0.x releases), we are comfortable making breaking changes to template-pack loading, as long as they are clearly documented and users can pin older versions if needed.
-
When we migrate the built-in DaisyUI templates into
powercrud_daisyui, we will:- Introduce the new pack-loader,
- Maintain a compatibility layer for existing
POWERCRUD_CSS_FRAMEWORKusage where feasible, and - Document the new
POWERCRUD_TEMPLATE_PACKsetting and requiredINSTALLED_APPSentries.- Future widget-registry work (
20251114_widget_registry.md) will extendPACK_STYLESwith awidgetssection but does not change the basic packaging story: widgets remain per-pack configuration within the samestyles.pyfile.
- Future widget-registry work (
-
Integration in downstream projects
- Projects enable a pack by:
- Adding the core app and pack app(s) to
INSTALLED_APPS(e.g.["powercrud", "powercrud_daisyui", ...]). - Setting
POWERCRUD_TEMPLATE_PACKto the desired pack name (e.g."daisyui").
- Adding the core app and pack app(s) to
- The real project base template (owned by the downstream project) is responsible for:
- Loading core JS once, e.g.
{% static 'powercrud/powercrud.js' %}. - Loading the active pack’s JS (and any CSS helpers), e.g.
{% static 'powercrud_daisyui/js/daisyui.js' %}. - Providing the actual HTML
<head>, navigation, and global assets; pack templates only render the inner CRUD views/partials.
- Loading core JS once, e.g.
- Projects enable a pack by:
-
Template-pack validator (contract compliance)
- Core will provide a small, developer-facing validation API to check that a pack complies with the template/JS/styles contract:
- A Python helper such as
validate_template_pack(pack_name="daisyui")that:- Verifies the pack app (
powercrud_<packname>) is importable and present inINSTALLED_APPS. - Verifies that required templates exist under the expected namespace (orchestrators and key partials like
partial/list.html,partial/detail.html,partial/bulk_edit_errors.html,bulk_edit_form.html,crispy_partials.html/partial/crispy_partials.html). - Optionally inspects templates for required
partialdefnames where core depends on them (e.g.pcrud_content,inline_row_display,inline_row_form,full_form,async_queue_success). - Verifies that
styles.pyexists and exportsPACK_STYLESwith at least the required top-level sections (filter_attrs, button styles, modal metadata).
- Verifies the pack app (
- An optional management command wrapper (e.g.
python manage.py validate_powercrud_pack d aisyui) that projects and CI can run explicitly.
- A Python helper such as
- The full validator is intended for development and tests, not as a heavy runtime check:
- The pack loader will still perform a lightweight runtime check (import app + styles) and raise
ImproperlyConfiguredif the pack cannot be loaded. - The richer “are all templates and partials present?” checks run via
validate_template_pack()and are wired into the test plan (Task 5.4) so official packs and community packs can assert contract compliance.
- The pack loader will still perform a lightweight runtime check (import app + styles) and raise
- Core will provide a small, developer-facing validation API to check that a pack complies with the template/JS/styles contract:
Related Future Work: Pluggable Form Widgets
This plan focuses on template packs and JavaScript structure. A closely related but separate project will be to make form and inline-form widgets pluggable (for example, the default HTML5 widgets and CSS classes currently configured in FormMixin.get_form_class()).
The intent is:
- keep the existing widget behavior stable during this refactor
- avoid introducing new hard-coded framework-specific classes into core templates or mixins
- design the template-pack contract so a future “widget policy” (per-pack or per-project) can control widget classes/attributes without breaking the public API
That future work would likely add a small, overridable hook (e.g. a get_default_widgets() / “widget policy” function) that template packs or projects can customize, building on top of the template-pack machinery defined here.