PowerCRUD Recipes
These recipes show how Base Configuration API options compose through class attributes, hooks, lists, and dictionaries.
Start with PowerCRUD Concepts if you want the mental model behind Surface, Field intent, Action, Presentation, Selection, Bulk operation, and Async operation.
For repeated field and action declarations, see Structured API Recipes.
Read-Only Review Surface
Use this when a team needs a searchable or filterable review screen, but should not edit records from the list.
Concepts involved:
- Surface
- Field intent
- Presentation
- Compatibility and defaults
from neapolitan.views import CRUDView
from powercrud.mixins import PowerCRUDMixin
class AuditEventCRUDView(PowerCRUDMixin, CRUDView):
model = AuditEvent
base_template_path = "core/base.html"
view_title = "Audit Events"
fields = [
"id",
"source",
"event_type",
"actor",
"created_at",
"severity",
]
detail_fields = "__all__"
filterset_fields = ["source", "event_type", "actor", "severity", "created_at"]
default_filterset_fields = ["source", "event_type", "severity"]
form_fields = ["pages"]
inline_edit_fields = ["pages"]
bulk_fields = []
bulk_delete = False
Notes:
fieldscontrols the list columns.filterset_fieldscontrols filtering, independently of list visibility.- Explicit empty lists make the no-edit intent clear.
Filtered Operational List
Use this when a list is an active working queue and users need focused default filters without losing access to the wider filter set.
Concepts involved:
- Surface
- Field intent
- Styling
class ProjectQueueCRUDView(PowerCRUDMixin, CRUDView):
model = Project
base_template_path = "core/base.html"
view_title = "Project Queue"
view_instructions = "Review open projects and update ownership or status."
fields = ["name", "owner", "status", "due_date", "priority"]
properties = ["is_overdue"]
filterset_fields = [
"owner",
"status",
"due_date",
"priority",
"customer",
]
default_filterset_fields = ["owner", "status", "priority"]
column_help_text = {
"is_overdue": "Calculated from due date and current status.",
}
show_record_count = True
paginate_by = 25
Notes:
default_filterset_fieldskeeps the first screen compact.- Users can still add optional filters from the configured filter allow-list.
propertiesare display fields, not editable model fields.
Annotated Operational Columns
Use this when an operational value is computed by the queryset and should appear in the list order as a sortable/filterable column.
For the queryset declaration details behind this pattern, see Queryset Annotation Fields.
Concepts involved:
- Surface
- Field intent
- Filtering
from django.db.models import BooleanField, Case, Value, When
class BookQueueCRUDView(PowerCRUDMixin, CRUDView):
model = Book
base_template_path = "core/base.html"
def get_queryset(self):
"""Expose the public annotation name used by PowerCRUD config."""
return super().get_queryset().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"]
filterset_fields = ["author", "long_book", "pages"]
default_filterset_fields = ["author", "long_book"]
column_help_text = {
"long_book": "Queryset annotation: true when pages is at least 400.",
}
form_fields = []
inline_edit_fields = []
bulk_fields = []
Notes:
- The name in
fieldsandfilterset_fieldsmust match the publicannotate(...)keyword. - Use
output_fieldso PowerCRUD can infer the generated filter type. - Annotation fields are read-only; keep them out of form, inline-edit, and bulk-edit config. Editable model fields such as
pagescan still be inline-editable on the same view. - Use
propertiesinstead when the value is Python-only display output and does not need queryset-backed filtering or list ordering.
Inline Editable Lookup Table
Use this for small reference tables where users should make quick row edits without opening a full form.
Concepts involved:
- Field intent
- Action
- Presentation
- Styling
class StatusCRUDView(PowerCRUDMixin, CRUDView):
model = Status
base_template_path = "core/base.html"
fields = ["name", "object_type", "sort_order", "is_active"]
detail_fields = "__all__"
form_fields = ["name", "object_type", "sort_order", "is_active"]
inline_edit_fields = ["name", "sort_order", "is_active"]
use_htmx = True
use_modal = True
inline_edit_always_visible = True
inline_edit_highlight_accent = "#14b8a6"
Notes:
inline_edit_fieldsis an editability setting.- Inline editable fields must be real editable model fields.
- Keep
form_fieldsbroad enough that inline saves can preserve required form state.
Row Action With Disabled Reason
Use this when a row-level operation is available only for some records and users need to know why it is unavailable.
Concepts involved:
- Action
- Presentation
- Field intent
class InvoiceCRUDView(PowerCRUDMixin, CRUDView):
model = Invoice
base_template_path = "core/base.html"
fields = ["number", "customer", "status", "total", "due_date"]
extra_actions_mode = "dropdown"
extra_actions = [
{
"url_name": "billing:invoice-send-reminder",
"text": "Send Reminder",
"button_class": "btn-accent",
"display_modal": True,
"needs_pk": True,
"disabled_if": "is_send_reminder_disabled",
"disabled_reason": "get_send_reminder_disabled_reason",
},
]
def is_send_reminder_disabled(self, obj, request) -> bool:
"""Return whether reminder sending should be disabled for this invoice."""
return obj.status != "overdue"
def get_send_reminder_disabled_reason(self, obj, request) -> str | None:
"""Return the tooltip reason when reminder sending is disabled."""
if self.is_send_reminder_disabled(obj, request):
return "Reminders can only be sent for overdue invoices."
return None
Notes:
- Keep the business rule in server-side hooks.
display_modalcontrols presentation, not whether the operation is allowed.- Use
extra_actions_mode = "dropdown"when row actions would crowd the table.
Selection-Aware Header Action
Use this when an operation works on the current selected row set.
Concepts involved:
- Selection
- Action
- Presentation
class CaseCRUDView(PowerCRUDMixin, CRUDView):
model = Case
base_template_path = "core/base.html"
fields = ["id", "title", "status", "assigned_to", "priority"]
bulk_delete = True
show_bulk_selection_meta = True
extra_buttons = [
{
"url_name": "cases:bulk-assign",
"text": "Assign Selection",
"button_class": "btn-primary",
"display_modal": True,
"uses_selection": True,
"selection_min_count": 1,
"selection_min_behavior": "disable",
"selection_min_reason": "Select at least one case first.",
},
]
Notes:
uses_selectionmeans the endpoint operates on the persisted PowerCRUD selection.selection_min_behavior = "disable"keeps the button visible but unavailable until enough rows are selected.- The endpoint should still validate the selection server-side.
Bulk Edit With Service-Backed Persistence
Use this when PowerCRUD should render the bulk edit UI, but application services own write logic.
Concepts involved:
- Bulk operation
- Action
- Field intent
- Compatibility and defaults
class TicketCRUDView(PowerCRUDMixin, CRUDView):
model = Ticket
base_template_path = "core/base.html"
fields = ["title", "status", "owner", "priority", "updated_at"]
form_fields = ["title", "status", "owner", "priority", "description"]
bulk_fields = ["status", "owner", "priority"]
bulk_delete = True
def persist_bulk_update(
self,
*,
queryset,
fields_to_update,
field_data,
progress_callback=None,
**kwargs,
):
"""Persist bulk ticket edits through the application service layer."""
return TicketUpdateService().persist_bulk_update(
queryset=queryset,
fields_to_update=fields_to_update,
field_data=field_data,
progress_callback=progress_callback,
default_persist=lambda **persist_kwargs: super(
TicketCRUDView,
self,
).persist_bulk_update(**persist_kwargs),
)
Notes:
bulk_fieldscontrols which model fields appear in the bulk edit form.- The service can enforce workflow locks, audit rules, notifications, or domain validation.
- Passing a
default_persistfallback lets the service delegate ordinary cases back to PowerCRUD.
Linked Cell Drill-In
Use this when a visible list cell should navigate to related detail without turning the whole row into a link.
Concepts involved:
- Field intent
- Presentation
- Surface
class BookCRUDView(PowerCRUDMixin, CRUDView):
model = Book
base_template_path = "core/base.html"
fields = ["title", "author", "published_date", "isbn"]
list_cell_link_default_open_in = "modal"
link_fields = {
"author": {
"view_name": "library:author-detail",
"pk_attr": "author_id",
"open_in": "modal",
"modal_box_classes": "modal-box flex max-h-[calc(100dvh-2rem)] w-11/12 max-w-4xl flex-col",
},
}
Notes:
link_fieldsapplies only to rendered list fields or properties.- Inline-editable cells are not linked; inline editing wins.
- Use
get_list_cell_link(...)when link metadata needs row-specific logic.
What To Do Next
Use these recipes as starting points, then consult:
- Setup & Core CRUD basics for the full first-view walkthrough.
- Configuration Options for accepted values and defaults.
- Hooks for method signatures and return contracts.
- Structured API Recipes when repeated field or action declarations would be clearer as
PowerField,PowerAction, orPowerButton.