Skip to content

Sample Application

Overview

The sample app provides a comprehensive demonstration of django-powercrud features using a realistic book/author management system. It serves as both a testing environment during development and a reference implementation for developers learning the package.

Presentation variants

The default sample uses the compatible DaisyUI pack. Its runtime metadata footer labels the active presentation. The focused-override presentation uses the same models, routes, data, permissions, and views, while prepending a small set of Book templates. The Bootstrap presentation uses that same sample application with the supported Bootstrap pack:

./manage.py runserver --settings=config.settings_focused_overrides 0:8001
./manage.py runbootstrap --port 8002

The runbootstrap command requires an explicit --port and starts Django's standard development server with config.settings_bootstrap, bound to all container interfaces. From the host, run it through the project wrapper, for example:

./runproj exec --command "cd src && ./manage.py runbootstrap --port 8003"

There is no in-application template-pack switcher. Start the desired settings configuration explicitly; use different ports when running presentations side by side. Bootstrap 5 is a supported non-default pack selected at process startup, while the unconfigured default remains DaisyUI. Its sample shell is deliberately compact and includes a light/dark theme selector so you can inspect both Bootstrap colour modes without changing the selected pack. See Selecting and configuring a template pack.

Models

The sample app includes four interconnected models that showcase different relationship types and field configurations:

  • Basic fields: name, bio, birth_date
  • Many-to-many: genres relationship used to constrain inline Book genre choices
  • Properties: has_bio, property_birth_date
  • Demonstrates property display in list/detail views
  • Core fields: title, author (ForeignKey), published_date, isbn, pages
  • Many-to-many: genres relationship
  • Advanced features:
  • isbn_empty GeneratedField for complex database expressions
  • uneditable_field for testing non-editable fields
  • Custom clean() and save() methods
  • Delayed delete() method for async testing
  • Properties with custom display names
  • Unique constraint on title + author
  • Simple model: name, description, numeric_string
  • Custom validation in clean() method
  • Guarded demo row for built-in Delete disable hooks
  • Protected demo row for handled single-delete refusal UX
  • Used for many-to-many relationships and filtering
  • OneToOneField to Author (tests 1:1 relationships)
  • ForeignKey to Genre (tests optional relationships)
  • Demonstrates related field handling in forms/filters

CRUD Views

Each model has a dedicated CRUD view demonstrating different powercrud features:

The menu pages use view_instructions only for a short description of the records shown in the sample app. Their expandable view_help content explains the PowerCRUD behaviour each page demonstrates in user-facing terms: what to click or create, what should happen, and where to look for the result.

BookCRUDView - Full Feature Demo

from powercrud.mixins import PowerCRUDAsyncMixin


class BookCRUDView(PowerCRUDAsyncMixin, CRUDView):
    # Comprehensive configuration showing:
    view_title = "My List of Books"
    view_instructions = "Browse and manage the sample book catalogue."
    view_help = {
        "summary": "About the Books demo",
        "details": (
            "This is the sample app's main, full-featured Books screen."
            "\n\n"
            "Open Filters or Cols to change the list, select rows to try summary and "
            "bulk controls, and use the row icons and three-dot menu for actions."
        ),
        "color": "info",
    }
    column_help_text = {
        "title": "The book title shown throughout the app.",
        "pages": "Demo link: opens this book detail in the current page.",
        "isbn": "Demo link: opens an external ISBN reference in a new tab or window.",
        "isbn_empty": "Shows whether this row currently has an ISBN value.",
        "description_empty": "Shows whether this row currently has description text.",
        "a_really_long_property_header_for_title": (
            "Demo link: opens the related author detail in a larger PowerCRUD modal."
        ),
    }
    list_cell_tooltip_fields = {
        "title": "get_title_tooltip",
        "pages": {"hook": "get_pages_tooltip", "mode": "lazy"},
        "isbn_empty": "get_isbn_empty_tooltip",
    }
    list_cell_link_default_open_in = "modal"
    list_options_enabled = True
    column_width_policy = "semantic"
    default_list_fields = [
        "title",
        "author",
        "published_date",
        "pages",
        "bestseller",
        "isbn",
        "genres",
        "isbn_empty",
        "a_really_long_property_header_for_title",
    ]
    link_fields = {
        "a_really_long_property_header_for_title": {
            "view_name": "sample:author-detail",
            "pk_attr": "author_id",
            "modal_presentation": {"size": "extra_wide"},
        },
        "pages": {
            "view_name": "sample:bigbook-detail",
            "open_in": "current",
        },
        "isbn": {
            "url": "https://www.isbn-international.org/content/what-isbn",
            "open_in": "new",
        },
    }
    form_class = BookForm
    form_display_fields = ["uneditable_field"]
    form_disabled_fields = ["isbn"]
    field_queryset_dependencies = {
        "genres": {
            "depends_on": ["author"],
            "filter_by": {"authors": "author"},
            "order_by": "name",
            "empty_behavior": "all",
        }
    }
    bulk_fields = ['title', 'published_date', 'bestseller', 'pages', 'author', 'genres']
    bulk_delete = True
    bulk_async = True

    filterset_fields = ['author', 'title', 'published_date', 'isbn', 'pages', 'description', 'genres']
    default_filterset_fields = ['author', 'title', 'published_date']
    filter_favourites_enabled = True
    dropdown_sort_options = {"author": "name"}
    inline_edit_fields = ['title', 'author', 'genres', 'published_date', 'bestseller', 'description']
    extra_actions_mode = "dropdown"
    row_actions_column_position = "end"
    row_actions_column_sticky = True

    extra_buttons = [...]  # Includes preserving and clear-on-success selection demos
    extra_actions = [...]  # Includes a conditional "Description Preview" demo

    def get_title_tooltip(self, obj, request=None):
        return f"{obj.author}\n{obj.pages} pages"

    def get_pages_tooltip(self, obj, request=None):
        return f"Page count: {obj.pages}"

    def get_isbn_empty_tooltip(self, obj, request=None):
        if obj.isbn_empty:
            return "This book does not currently have an ISBN."
        return f"ISBN: {obj.isbn}"

The sample BookCRUDView uses view_title = "My List of Books" plus a concise view_instructions description of the sample catalogue. Its expandable view_help contains the PowerCRUD-oriented walkthrough, demonstrating collapsed screen-level guidance with a one-line summary, escaped paragraph text, a subtle info colour tint, and table-aligned width. The Books list demonstrates restricted page-size controls with page_size_options = [5, 10, 25, 50] and page_size_all_enabled = False, so oversized 100 and All choices are not offered. Its wide table explicitly keeps row actions at logical end and pins that column horizontally, including inline Save/Cancel controls and the extras-only row kebab. At narrow widths, the same row instead demonstrates the automatic all-actions kebab. The column_help_text mapping covers fields and properties so the sample list shows the header-help tooltip pattern; on linked demo columns, the header help explicitly says whether the link opens in the current page, a new tab/window, or the PowerCRUD modal. list_cell_tooltip_fields maps selected fields/properties to row-specific tooltip hooks for the inline-editable title, the visible non-inline pages field, and the boolean-like isbn_empty property cell. The pages tooltip uses mode="lazy" so its content is fetched only when the page-count cell is hovered or focused. The optional description_empty property column can be added through Cols when checking lazy row-action availability for books with no description. The sample title tooltip intentionally uses a newline so the demo shows multiline semantic list-cell tooltip rendering, while header-help tooltips and other tooltip surfaces keep their normal single-line behavior. That changes only the list surface above and inside the table; other UI copy such as the create button still comes from the model verbose names, and the instructions text, collapsed screen help, header help text, and semantic cell tooltip text are all rendered as plain escaped text rather than HTML.

The same sample view now also demonstrates list-cell linking through the narrow declarative link_fields API. The live sample uses the non-inline property column a_really_long_property_header_for_title so the screen can keep its primary title and author columns reserved for inline-edit and dependency demos. That is deliberate: PowerCRUD never turns inline-editable cells into links. The sample sets list_cell_link_default_open_in = "modal" and uses the dict form with pk_attr = "author_id" plus modal_presentation, so that existing non-inline link opens the related author detail through a noticeably larger PowerCRUD modal when the sample page is running with modal support. In views that omit list_cell_link_default_open_in, PowerCRUD assumes "new". The sample links pages to the current book detail with explicit open_in = "current", and keeps isbn out of inline_edit_fields so that visible field can link to a static external ISBN reference with explicit open_in = "new".

The same sample view now also demonstrates progressive filter visibility:

  • author, title, and published_date are visible by default through default_filterset_fields
  • isbn, pages, description, and genres remain allowed filters but start hidden
  • the Add filter control reveals those optional filters on demand without changing the underlying filterset contract

The same sample view now also demonstrates list options:

  • list_options_enabled = True enables Cols, while default_list_fields keeps the initial book table narrower than the full allowed column set
  • users can open Cols and add allowed hidden columns such as description_empty and uneditable_field
  • the current column choice is scoped to the browser session and the BookCRUDView
  • reset returns the table to the declared default_list_fields

The sample BookCRUDView also demonstrates the optional saved-favourites contrib app:

  • filter_favourites_enabled = True turns on the toolbar for this list
  • saved favourites persist the current filters, optional filter visibility, sort, page size, and visible columns for the signed-in user, scoped to the list view's derived identity
  • the sample project mounts include("powercrud.urls", namespace="powercrud"), which is required for the optional favourites endpoints

See Filtering for the core filter behavior and Saved Favourites for the optional contrib add-on.

AnnotatedBookCRUDView - Queryset Annotation Columns

The sample app includes a focused list-only view at /sample/annotated-book/ for queryset-backed list/filter fields.

from django.db.models import BooleanField, Case, Value, When
from powercrud.actions import PowerButton

OPEN_BOOK_ICON_SVG = """
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
    <path stroke-linecap="round" stroke-linejoin="round" d="M4 5.5A2.5 2.5 0 0 1 6.5 3H20v16H6.5A2.5 2.5 0 0 0 4 21.5v-16Z" />
</svg>
"""


class AnnotatedBookCRUDView(PowerCRUDAsyncMixin, CRUDView):
    model = Book
    url_base = "annotated-book"

    queryset = Book.objects.select_related("author").annotate(
        long_book=Case(
            When(pages__gte=400, then=Value(True)),
            default=Value(False),
            output_field=BooleanField(),
        )
    )
    fields = ["title", "author", "pages", "long_book", "published_date"]
    list_options_enabled = True
    default_list_fields = ["title", "author", "pages", "published_date"]
    filterset_fields = ["author", "long_book", "pages"]
    default_filterset_fields = ["author", "long_book"]
    inline_edit_fields = ["pages"]
    bulk_fields = []
    bulk_delete = False
    extra_actions_mode = "buttons"
    row_actions_column_position = "start"
    extra_buttons = [
        PowerButton(
            text="Annotated Selection Summary",
            url_name="sample:annotated-book-selected-summary",
            display_modal=True,
            uses_selection=True,
            selection_min_count=1,
            selection_min_behavior="disable",
        )
    ]
    extra_actions = [
        {
            "url_name": "sample:bigbook-detail",
            "text": "Open Book",
            "display_modal": True,
            "icon_svg": OPEN_BOOK_ICON_SVG,
            "icon_only": True,
        }
    ]

The long_book column is not a model field. It is the public queryset annotation name, and PowerCRUD uses that same name in fields, generated filters, sorting, header help, cell tooltips, and list-column selection. The sample sets list_options_enabled = True and keeps long_book out of default_list_fields so it appears as an optional selectable column in the Cols control. The sample makes the real pages model field inline-editable while keeping long_book out of inline edit and bulk edit config because annotation fields are read-only.

The same annotated list also demonstrates selection controls for a selection-aware toolbar button without enabling built-in bulk edit/delete. Annotated Selection Summary uses uses_selection=True, while bulk_fields = [] and bulk_delete = False, so row selection exists solely for the custom modal endpoint. Its extra_actions_mode = "buttons" setting keeps the configured Open Book action directly visible on wider screens, making this the sample catalogue's explicit button-mode example. OPEN_BOOK_ICON_SVG is a named source-code constant and icon_only=True turns that direct action into an accessible icon-only control. The logical-start Actions column appears immediately after the default-sticky selection column; action-column stickiness remains disabled here so relocation can be inspected independently. See Queryset Annotation Fields for the declaration details behind this sample.

The Authors list combines extra_actions_mode = "all_dropdown" with a logical-start sticky action column. Its quiet vertical-ellipsis trigger pins immediately after the default-sticky selection checkbox, the visible header label is omitted, and the menu is one labelled list: View, Edit, Home, View Again, then Delete. Inline Save/Cancel temporarily replace that trigger and the compact menu returns after the HTMX row replacement.

The PowerField Books list provides the opposite-edge comparison: it uses the same compact all-actions menu on a logical-end sticky column. Its reusable Normal Edit PowerAction has NORMAL_EDIT_ICON_SVG, while the derived Description Preview action explicitly removes that icon. The resulting menu demonstrates a shared icon gutter for mixed standard and configured actions. The main Books list deliberately remains on the extras-only "dropdown" mode so both menu choices can be inspected against equivalent Book fields and default columns.

The Async list has no standard row actions. Its View Progress extra action uses VIEW_PROGRESS_ICON_SVG, demonstrating that an extras-only dropdown gains an icon gutter when an extra supplies an icon. A dropdown with no icons has no empty gutter.

The main Books list has two selected-summary toolbar demos. Selected Summary reads the current selection and uses the default selection-aware behavior, so PowerCRUD clears the persisted selection after the HTMX request succeeds. Selected Summary (Do Not Clear) reads the same selection but sets clear_selection_on_success=False, so the modal can preview selected rows without clearing them.

The sample frontend now also shows the downstream tooltip-styling path. In src/config/static/css/app.custom.css, the sample app actively overrides --pc-tooltip-bg and --pc-tooltip-fg to use DaisyUI's primary semantic tokens, while PowerCRUD itself keeps neutral tooltip defaults. The sample Vite entry imports that file after powercrud/css/powercrud.css, so readers can inspect the real app-level override pattern rather than only reading about it in the styling guide.

The sample form configuration now also demonstrates two contextual form-surface features:

  • form_display_fields = ["uneditable_field"] shows the model’s non-editable field in a separate read-only Context block above the update form.
  • form_disabled_fields = ["isbn"] keeps the ISBN visible on update forms but locks the input so users can see it without changing it.

BookForm remains the source of truth for editable inputs, while PowerCRUD layers the display-only context block and disabled-field behaviour on top of that custom form. It deliberately leaves genres on Django's silent model-backed widget path, so the selected pack can apply its multiselect presentation, while its explicit published_date = DateInput(type="date") widget remains unchanged. Normal forms, filters, and bulk editing use the standard multiselect variant; inline editing uses the compact variant with an N selected summary. Enhanced multiselects show checked options, support click-to-toggle and clear-all, and preserve the submitted values and dependency rules.

The sample BookCRUDView now also demonstrates both custom action enhancements discussed in the docs:

  • a selection-aware extra_button that opens a modal summary for the current persisted PowerCRUD selection
  • permission-hidden built-in Create/Detail/Edit/Delete affordances for the sample viewer user
  • permission-hidden toolbar and row actions for the sample viewer user
  • a row-level extra_action that disables itself with a tooltip when the book has no description
  • an opt-in modal extra_action using refresh_list_on_modal_close=True to refresh the current list when its modal is closed
  • portable per-trigger modal sizing on a modal list-cell link, a modal extra_button, and modal extra_actions through modal_presentation
  • semantic field-level list-cell tooltips on the inline title, non-inline pages, and isbn_empty property columns
  • session-backed list-column choices through Cols
  • declarative list-cell linking on pages (current), visible isbn (new), and the non-inline Really Long Title property column (modal)
  • an active sample app-level tooltip theme override through --pc-tooltip-bg / --pc-tooltip-fg
  • a guarded row (Guarded Sample Book) that disables the built-in Edit action and inline editing before the user can start an update
  • a bulk-validation demo row (Bulk Validation Sample Book) that re-renders the bulk edit modal for sync bulk updates and fails the async task for queued bulk updates when a sample bulk rule is violated

These examples are intentionally simple so package users can inspect both the view config and the matching sample endpoints/templates.

PowerFieldBookCRUDView - Field Intent Helper Variant

The sample app includes a sibling Book view at /sample/powerfield-book/ labelled PowerField Books.

This view uses power_fields instead of Base API Field Intent attributes. It is not a subclass of BookCRUDView, because PowerCRUD rejects mixing base Field Intent and PowerField declarations in one inheritance chain. Like the main Books view, it opts into semantic list widths.

from powercrud.actions import PowerAction, PowerButton
from powercrud.powerfields import PowerField, PowerOverride


class PowerFieldBookCRUDView(PowerCRUDAsyncMixin, CRUDView):
    model = Book
    namespace = "sample"
    url_base = "powerfield-book"
    view_instructions = "Browse and manage the same book catalogue as the Books page."
    view_help = {
        "summary": "About the Power* Structured API demo",
        "details": (
            "This page is intended to behave like the main Books page. The source code "
            "declares the same setup through PowerField, PowerButton, and PowerAction."
            "\n\n"
            "Open a row's three-dot button to see the only intentional visible "
            "difference: every row action is inside that menu."
        ),
        "color": "info",
    }
    list_options_enabled = True
    column_width_policy = "semantic"
    extra_actions_mode = "all_dropdown"
    row_actions_column_position = "end"
    row_actions_column_sticky = True
    form_class = BookForm

    power_fields = [
        PowerOverride(list="__all__", detail="__all__"),
        PowerField(
            "title",
            default_list=True,
            tooltip_hook="get_title_tooltip",
            form=True,
            inline=True,
            bulk=True,
        ),
        PowerField(
            "author",
            default_list=True,
            form=True,
            inline=True,
            bulk=True,
        ),
        PowerField(
            "published_date",
            default_list=True,
            form=True,
            inline=True,
            bulk=True,
        ),
        PowerField(
            "pages",
            default_list=True,
            tooltip_hook="get_pages_tooltip",
            form=True,
            bulk=True,
            link={
                "view_name": "sample:powerfield-book-detail",
                "open_in": "current",
            },
        ),
        PowerField(
            "isbn_empty",
            property=True,
            detail_property=True,
            default_list=True,
            tooltip_hook="get_isbn_empty_tooltip",
        ),
        PowerField(
            "description_empty",
            property=True,
            detail_property=True,
        ),
        PowerField(
            "description",
            form=True,
            inline=True,
            exclude={"list": True},
        ),
        PowerField("uneditable_field", form_display=True),
    ]

    def get_title_tooltip(self, obj, request=None):
        return f"{obj.author}\n{obj.pages} pages"

    def get_pages_tooltip(self, obj, request=None):
        return f"Page count: {obj.pages}"

    def get_isbn_empty_tooltip(self, obj, request=None):
        if obj.isbn_empty:
            return "This book does not currently have an ISBN."
        return f"ISBN: {obj.isbn}"

    row_modal = PowerAction(
        text="Normal Edit",
        url_name="sample:bigbook-update",
        display_modal=True,
        permission_check="can_manage_books",
        permission_behavior="hide",
    )

    extra_actions = [
        row_modal,
        row_modal.with_options(
            text="Description Preview",
            url_name="sample:bigbook-description-preview",
            permission_check="can_preview_description",
            permission_behavior="hide",
            hidden_if="should_hide_description_preview",
            hidden_if_mode="lazy",
            disabled_state="get_description_preview_disabled_state",
            disabled_state_mode="lazy",
        ),
    ]

    extra_buttons = [
        PowerButton(
            text="Selected Summary",
            url_name="sample:bigbook-selected-summary",
            display_modal=True,
            uses_selection=True,
            selection_min_count=1,
            selection_min_behavior="disable",
            permission_check="can_use_selected_summary",
            permission_behavior="hide",
        ),
        PowerButton(
            text="Selected Summary (Do Not Clear)",
            url_name="sample:bigbook-selected-summary",
            display_modal=True,
            uses_selection=True,
            clear_selection_on_success=False,
            selection_min_count=1,
            selection_min_behavior="disable",
            permission_check="can_use_selected_summary",
            permission_behavior="hide",
        ),
    ]

The real sample view is more complete than this excerpt. It mirrors the base BookCRUDView list-column contract exactly: PowerOverride(list="__all__") exposes the same available model columns, the description declaration applies the same list exclusion, and default_list=True declarations produce the same initially visible columns. It intentionally differs only in row-action presentation: the PowerField variant uses the compact all-actions menu while retaining the same logical-end sticky column. It links to its own sample:powerfield-book-detail route so the sample remains self-contained, and it mirrors the BookCRUDView toolbar buttons and row actions through PowerButton and PowerAction, including a with_options(...) row-action variant.

See Choosing an API Style, PowerField, and PowerField Reference for the constructor and validation contract.

The sample BookCRUDView also includes illustrative persistence-hook wiring:

  • persist_single_object(...)
  • persist_bulk_update(...)
  • bulk_update_persistence_backend_path = "sample.backends.BookBulkUpdateBackend"

The single-object hook stays intentionally thin. For bulk updates, the sync hook and the async backend both route through BookBulkUpdateService, so the same sample validation rule applies in either execution mode.

The sample app also now includes tutorial-oriented helper classes in sample.services and sample.backends:

  • BookWriteService
  • BookBulkUpdateService
  • BookBulkUpdateBackend

These are deliberately small examples used by the advanced persistence-hook guides. They are there to make the documentation more inspectable, and BookBulkUpdateBackend is now also wired into the sample BookCRUDView so the async bulk example is real rather than purely illustrative.

Inline dependency demo

The sample app now includes a concrete inline dependency example:

  • Book.author is the parent field.
  • Book.genres is the dependent field.
  • Allowed genre choices come from Author.genres, not from historical book rows.

field_queryset_dependencies is the primary declaration for this rule, so the same queryset restriction applies to regular forms and inline editing. BookForm stays in place only for form-specific tweaks such as keeping genres optional when an author has no allowed genres.

Worked configuration:

field_queryset_dependencies = {
    "genres": {
        "depends_on": ["author"],
        "filter_by": {"authors": "author"},
        "order_by": "name",
        "empty_behavior": "all",
    }
}

How to read that:

  • genres is the child field being restricted
  • author is the parent form field the user changes
  • authors is the queryset lookup on Genre

So the child queryset is effectively narrowed as if PowerCRUD were doing:

Genre.objects.filter(authors=<selected author>).order_by("name")

That same rule applies in two places:

  • the normal Book create/edit form
  • inline editing on the Books list

When the user changes author inline, PowerCRUD posts the current row data to the dependency endpoint, rebuilds the genres widget through the same form pipeline, and swaps the refreshed widget back into the row.

Other Views

  • GenreCRUDView: Its help explains how to edit directly in the table and tells users to create, or use, records named exactly Guarded Sample Genre and Protected Sample Genre to try the two different Delete refusal paths
  • ProfileCRUDView: Its help explains how to reveal optional columns, edit or bulk-edit profile values, and create a Genre whose name begins with S when another favourite-genre choice is needed
  • AuthorCRUDView: Its help tells users to scroll the wide table, open the pinned three-dot menu, and start inline editing to see Save and Cancel replace that menu temporarily. Its list-level Genres link demonstrates a per-view extra_buttons_dropdown_label = "More" override.
  • BookCRUDView: Async bulk editing, dependent author -> genres queryset scoping, restricted page-size options without All, view_title / view_instructions / view_help heading-area overrides, column_help_text header tooltips, list options through Cols, semantic field-level list-cell tooltips on inline and non-inline columns, declarative modal and external list-cell link demos, permission-aware Create/Detail/Edit/Delete and custom action affordances, default clear-on-success and explicit opt-out selection-aware extra_buttons in the top toolbar overflow menu, an end-positioned sticky row-actions column, extras-only dropdown row actions that open upward for the last five rendered rows, and a guarded sample row for built-in Edit and inline update guards
  • PowerFieldBookCRUDView: Its help asks users to compare it with Books and explains both the developer-facing Power* Structured API and the one visible difference: all row actions are inside the pinned three-dot menu
  • AnnotatedBookCRUDView: Its help explains that Long Book means at least 400 pages, how to reveal it through Cols, how changing Pages recalculates it, and how selection and the direct Open Book action differ from the main Books page
  • AsyncTaskRecordCRUDView: Its help tells users how to create a background-job record from a bulk edit on Books, explains the different date/time formats, and points to View Progress in the row's three-dot menu

The Genre sample keeps these delete demos deliberately narrow. If the named row is not already present, create it from the Genres page; the name must match exactly:

  • If a row is named Guarded Sample Genre, GenreCRUDView.can_delete_object(...) returns False and the built-in Delete action renders disabled with a tooltip reason before the modal opens.
  • If a row is named Protected Sample Genre, its delete() method raises ValidationError("Protected Sample Genre exists to demonstrate handled delete refusals.").

That lets the sample app show both layers of the product story on the same lightweight CRUD surface:

  • pre-click Delete disablement
  • post-click handled delete refusal

The Book sample includes a separate update-guard demo on the busier inline-editing screen:

  • If a row is titled Guarded Sample Book, BookCRUDView.can_update_object(...) returns False.
  • The built-in Edit action renders disabled with a tooltip reason.
  • Inline-editable cells stay visible but render as disabled affordances with the same reason, so the sample shows both update surfaces together on a screen that already exercises inline editing.

The same Book screen now also includes a bulk-validation demo:

  • If a selected row is titled Bulk Validation Sample Book, BookBulkUpdateService rejects a sample bulk bestseller=true update.
  • If the selection stays below bulk_min_async_records, PowerCRUD re-renders the bulk edit modal with the handled error payload instead of treating the result as a server failure.
  • If the selection reaches the async threshold, the queued task fails instead and the sample async dashboard shows the failure state.
  • That makes BookCRUDView the sample app reference for shared sync/async bulk persistence wiring plus the current difference between sync modal errors and async task-level failures.

ProfileCRUDView alignment and queryset demo

ProfileCRUDView is the sample app reference for two smaller-but-practical list customizations:

  • mixed per-column list alignment through column_alignments
  • static queryset rules shared across normal forms, inline editing, and bulk edit choices

The view uses three list columns to demonstrate the alignment feature in a way that is easy to inspect on screen:

  • status is a short categorical value and is centered
  • priority_band is a short categorical value and is right-aligned
  • favorite_genre is ordinary text and is kept left-aligned

Current sample config:

column_alignments = {
    "status": "center",
    "priority_band": "right",
    "favorite_genre": "left",
}

That same screen also keeps those fields in inline editing and bulk editing, so the sample shows how alignment overrides behave across the main list display states without moving the feature onto the much busier Book screen.

Profile also carries the static queryset demo for favorite_genre:

field_queryset_dependencies = {
    "favorite_genre": {
        "static_filters": {"name__startswith": "S"},
        "order_by": "name",
    }
}

How to read that:

  • favorite_genre is the field being restricted
  • static_filters applies a fixed queryset rule with no parent field involved
  • order_by keeps the remaining choices sorted predictably

That same static rule is reused in three places:

  • the normal Profile create/edit form
  • inline editing on the Profiles list
  • the bulk edit dropdown for favorite_genre

This makes ProfileCRUDView the sample app reference for static queryset rules and mixed list alignment overrides, while BookCRUDView remains the reference for dynamic parent/child dependencies.

Example BookCRUDView action config:

extra_actions_mode = "dropdown"
extra_buttons_mode = "dropdown"
extra_actions_dropdown_open_upward_bottom_rows = 5

extra_buttons = [
    {
        "url_name": "home",
        "text": "Home in Modal!",
        "display_modal": True,
        "modal_presentation": {"max_width": "48rem"},
    },
    {
        "url_name": "sample:bigbook-selected-summary",
        "text": "Selected Summary",
        "display_modal": True,
        "uses_selection": True,
        "selection_min_count": 1,
        "selection_min_behavior": "disable",
        "permission_check": "can_use_selected_summary",
        "permission_behavior": "hide",
    },
    {
        "url_name": "sample:bigbook-selected-summary",
        "text": "Selected Summary (Do Not Clear)",
        "display_modal": True,
        "uses_selection": True,
        "clear_selection_on_success": False,
        "selection_min_count": 1,
        "selection_min_behavior": "disable",
        "permission_check": "can_use_selected_summary",
        "permission_behavior": "hide",
    },
]

extra_actions = [
    {
        "url_name": "sample:bigbook-description-preview",
        "text": "Description Preview",
        "needs_pk": True,
        "display_modal": True,
        "permission_check": "can_preview_description",
        "permission_behavior": "hide",
        "hidden_if": "should_hide_description_preview",
        "hidden_if_mode": "lazy",
        "disabled_state": "get_description_preview_disabled_state",
        "disabled_state_mode": "lazy",
        "modal_presentation": {"size": "extra_wide"},
    },
]

That lets the sample app demonstrate permission-hidden header actions, default clear-on-success selection-aware header actions, an explicit selection-preserving opt-out, hidden row actions, conditionally disabled row actions, lazy dropdown hidden-state and disabled-state hydration, and per-trigger modal sizing in the same CRUD surface. permission_check omits actions before row or selection state is evaluated. hidden_if omits a row action when it is not applicable. hidden_if_mode = "lazy" keeps that relevance check out of the initial list render and resolves it when the row menu opens. disabled_state is the single-hook disabled contract: return a non-empty string to disable the action and show that string as the reason. disabled_state_mode = "lazy" keeps that exact disabled reason out of the initial list render and resolves it when the row menu opens. Selected Summary uses the view default modal width and demonstrates the default clear-on-success behavior, while Selected Summary (Do Not Clear) demonstrates clear_selection_on_success=False. Home in Modal! shows a header-button maximum-width override. The modal_presentation entries are portable partial mappings that merge with the supplied viewport-bounded defaults.

The top-left sample login menu includes a viewer and manager:

  • sample-viewer can open the Book lists but does not see built-in Create/Detail/Edit/Delete, bulk update/delete, row selection controls, or permission-hidden custom actions.
  • sample-manager sees those affordances, and then row, selection, or bulk state can still disable controls with tooltip reasons.

Management Commands

# Create default authors (25) and books (50)
./manage.py create_sample_data

# Create 100 authors and 1000 books
./manage.py create_sample_data --authors 100 --books 1000

# Create 500 books with an average of 5 books per author
./manage.py create_sample_data --books 500 --books-per-author 5

Generates realistic sample data:

  • Random author names and bios
  • Book titles, descriptions, publication dates, and ISBNs
  • Progress feedback during creation
  • Allows control over the distribution of books per author using --books-per-author.
./manage.py clear_sample_data --all        # Delete everything
./manage.py clear_sample_data --books      # Books only
./manage.py clear_sample_data --authors    # Authors only (cascades to books)

Safety features:

  • Only works when DEBUG=True
  • Handles protected foreign key relationships
  • Provides clear feedback on deletion counts

Forms & Filters

Custom Forms

  • BookForm: Field selection and form-specific tweaks while field_queryset_dependencies handles the shared author -> genres queryset rule. The silent genres field receives the selected pack's standard multiselect presentation, while the explicit published_date date widget remains application-owned.
  • AuthorForm: Demonstrates form customization patterns

Advanced Filtering

  • BookFilterSet: HTMX integration, custom widget attributes
  • AuthorFilterSet: Inherits from HTMXFilterSetMixin for reactive filtering
  • Shows crispy forms layout integration

Development Use Cases

Feature Testing

  • Bulk Operations: Test edit/delete on multiple books with validation
  • Async Processing: Book deletion includes artificial delay for async testing
  • Complex Relationships: M2M genres, ForeignKey authors, OneToOne profiles
  • Field Types: Generated fields, boolean displays, date formatting

UI/UX Testing

  • Modal Interactions: All CRUD operations in modals
  • HTMX Features: Reactive filtering, pagination, form updates
  • Inline Dependencies: Changing a Book author inline immediately refreshes the allowed genre choices derived from the shared form dependency config
  • Inline Validation Errors: Clear a Book title inline and save to see the row stay open with field-level error text and a field popover
  • Static Queryset Rules: Editing a Profile only offers favorite_genre choices whose names start with S, and the same restriction carries through inline and bulk edit
  • Template packs: Run the same sample with DaisyUI or Bootstrap 5; the compact Bootstrap shell includes a light/dark theme selector
  • Responsive Design: Semantic Book and PowerField Book column widths alongside explicit per-column modes elsewhere in the sample

Manual Inline Error Repro

Use the Book list to inspect the inline validation UI:

  1. Open /sample/bigbook/.
  2. Click the inline edit affordance on a book title.
  3. Clear the title field.
  4. Click Save.

Expected result: the row remains in edit mode, the title field is marked invalid, and a forced-visible popover says This field is required.. The inline error text remains in the markup as an accessibility/fallback message but is visually hidden while the popover is active.

To check inline searchable selects on the same screen, click the author inline-edit affordance. The field should focus and open the dropdown.

Configuration Examples

  • Property Display: Custom property names and formatting
  • Field Exclusions: Hide sensitive/internal fields
  • Custom Actions: Additional buttons and row-level actions, including dropdown-style overflow for extra_actions
  • Sorting & Filtering: Advanced queryset manipulation

Getting Started

  1. Run migrations:
./manage.py migrate
  1. Create sample data:
./manage.py create_sample_data
  1. Access the views:

  2. Books: http://localhost:8001/sample/bigbook/

  3. Authors: http://localhost:8001/sample/author/
  4. Genres: http://localhost:8001/sample/genre/
  5. Profiles: http://localhost:8001/sample/profile/

  6. Test features:

  7. Try bulk edit operations on books

  8. Use filtering and sorting
  9. Test modal create/edit/delete
  10. Open a Book row inline, change author, and confirm genres refreshes immediately without saving
  11. Experiment with the restricted Books page sizes

How to try the inline dependency demo

  1. Open the Books list at /sample/bigbook/.
  2. Edit an Author and assign one or more genres to that author.
  3. Open a Book edit form in a modal and confirm the genres dropdown only shows genres for that author.
  4. Open a Book row in inline mode.
  5. Change the Book author.
  6. Re-open the Book genres control before saving.
  7. Confirm the available genres now match the selected author’s genres relation.

How to adapt this pattern downstream

If your project used an older inline-only dependency pattern, the sample app demonstrates the preferred replacement:

field_queryset_dependencies = {
    "cmms_asset": {
        "depends_on": ["cmms_property_asset_type_override"],
        "filter_by": {
            "property_asset_type_override": "cmms_property_asset_type_override",
        },
        "empty_behavior": "none",
    }
}

The key point is that filter_by maps:

  • queryset lookup on the child field's queryset model
  • to parent form field name

Inline refresh wiring is derived automatically from this declaration.

The browser regression for this flow lives in test_inline_dependencies.py.

Development Notes

The sample app is designed to be:

  • Comprehensive: Covers all major powercrud features
  • Realistic: Uses believable domain models and relationships
  • Educational: Clear examples of configuration patterns
  • Extensible: Easy to add new models or features for testing

When developing new powercrud features, add corresponding examples to the sample app to ensure comprehensive testing coverage.