Complete Example
This page shows a deliberately feature-rich PowerCRUDMixin view so you can see current configuration syntax in one place. It is not intended as a recommended starting point; most projects should begin much smaller and add options only when needed.
from django.db.models import BooleanField, Case, Value, When
from neapolitan.views import CRUDView
from powercrud.mixins import PowerCRUDMixin
from . import models
from .forms import ProjectForm
class ProjectCRUDView(PowerCRUDMixin, CRUDView):
# ------------------------------------------------------------------
# Core model / URLs
# ------------------------------------------------------------------
model = models.Project
namespace = "projects"
url_base = "active-project"
def get_queryset(self):
"""Expose a public queryset annotation for list/filter/sort use."""
return super().get_queryset().annotate(
needs_attention=Case(
When(status="blocked", then=Value(True)),
default=Value(False),
output_field=BooleanField(),
)
)
# ------------------------------------------------------------------
# Templates / base rendering
# ------------------------------------------------------------------
base_template_path = "core/base.html"
templates_path = "projects/powercrud"
view_title = "Active Client Projects" # visible list heading only
view_instructions = "Use the list below to review and update active projects."
view_help = {
"summary": "About this screen",
"details": (
"Use this screen to review active projects and update ownership, "
"status, and priority.\n\n"
"Bulk actions apply to the selected rows, while filters and list "
"columns change the current working view."
),
"color": "info",
}
column_help_text = {
"owner": "Business owner responsible for the project.",
"display_status": "Calculated status shown for quick triage.",
"needs_attention": "Queryset annotation used for triage filtering.",
}
column_alignments = {
"status": "center",
"needs_attention": "center",
}
list_cell_tooltip_fields = {
"owner": "get_owner_tooltip",
"is_overdue": "get_is_overdue_tooltip",
"needs_attention": "get_needs_attention_tooltip",
}
list_cell_link_default_open_in = "modal"
link_fields = {
"owner": "crm:owner-detail",
"reference_code": {
"view_name": "projects:project-detail",
"modal_box_classes": "modal-box flex max-h-[calc(100dvh-2rem)] w-11/12 max-w-5xl flex-col",
},
"is_overdue": {
"url": "https://docs.example.com/projects",
"open_in": "new",
},
}
# ------------------------------------------------------------------
# HTMX / modal behaviour
# ------------------------------------------------------------------
use_htmx = True
use_modal = True
default_htmx_target = "#content"
modal_id = "projectModal"
modal_target = "projectModalContent"
modal_box_classes = "modal-box flex max-h-[calc(100dvh-2rem)] w-11/12 max-w-4xl flex-col"
bulk_modal_box_classes = "modal-box flex max-h-[calc(100dvh-2rem)] w-11/12 max-w-6xl flex-col"
hx_trigger = {
"projectsChanged": True,
"refreshSidebar": True,
}
# ------------------------------------------------------------------
# List, detail, and form scopes
# ------------------------------------------------------------------
fields = [
"reference_code",
"owner",
"project_manager",
"status",
"needs_attention",
"due_date",
]
exclude = ["internal_notes"]
properties = ["is_overdue", "display_status"]
properties_exclude = ["display_status"]
list_options_enabled = True
default_list_fields = [
"reference_code",
"owner",
"status",
"needs_attention",
"is_overdue",
]
detail_fields = "__fields__"
detail_exclude = ["internal_notes"]
detail_properties = "__properties__"
detail_properties_exclude = []
form_class = ProjectForm
form_display_fields = ["reference_code", "created_by"]
form_disabled_fields = ["status"]
# ------------------------------------------------------------------
# Filtering / dropdown behaviour
# ------------------------------------------------------------------
filterset_fields = [
"owner",
"project_manager",
"status",
"needs_attention",
"due_date",
"tags",
]
default_filterset_fields = ["owner", "status", "needs_attention"]
filter_favourites_enabled = True
filter_null_fields_exclude = ["due_date"]
dropdown_sort_options = {
"owner": "name",
"project_manager": "name",
"status": "label",
}
m2m_filter_and_logic = True
searchable_selects = True
# Auto-generated text filters use icontains; switch to filterset_class
# if you need per-field lookup expressions such as iexact or startswith.
field_queryset_dependencies = {
"tags": {
"depends_on": ["owner"],
"filter_by": {"owners": "owner"},
"order_by": "name",
"empty_behavior": "none",
},
"project_manager": {
"static_filters": {"is_active": True},
"order_by": "name",
}
}
# ------------------------------------------------------------------
# Inline editing
# ------------------------------------------------------------------
inline_edit_fields = ["status", "project_manager", "due_date"]
inline_edit_always_visible = True
inline_edit_highlight_accent = "#14b8a6"
inline_edit_requires_perm = "projects.change_project"
inline_preserve_required_fields = True
# ------------------------------------------------------------------
# Bulk editing
# ------------------------------------------------------------------
bulk_fields = ["status", "project_manager", "tags"]
bulk_delete = True
bulk_full_clean = True
# ------------------------------------------------------------------
# Pagination / metadata
# ------------------------------------------------------------------
paginate_by = 25
show_record_count = True
show_bulk_selection_meta = True
# ------------------------------------------------------------------
# Table styling
# ------------------------------------------------------------------
table_pixel_height_other_page_elements = 96
table_max_height = 75
table_max_col_width = 30
table_header_min_wrap_width = 18
table_classes = "table-sm"
action_button_classes = "btn-sm"
extra_button_classes = "btn-sm"
extra_buttons_mode = "dropdown"
extra_actions_mode = "dropdown"
extra_actions_dropdown_open_upward_bottom_rows = 3
# ------------------------------------------------------------------
# Extra buttons / row actions
# ------------------------------------------------------------------
extra_buttons = [
{
"url_name": "projects:dashboard",
"text": "Dashboard",
"button_class": "btn-info",
"needs_pk": False,
"display_modal": False,
"htmx_target": "content",
},
{
"url_name": "projects:project-report",
"text": "Summary Report",
"button_class": "btn-accent",
"needs_pk": False,
"display_modal": True,
"uses_selection": True,
"selection_min_count": 1,
"selection_min_behavior": "disable",
"selection_min_reason": "Select at least one project first.",
"refresh_list_on_modal_close": True,
"modal_box_classes": "modal-box flex max-h-[calc(100dvh-2rem)] w-11/12 max-w-5xl flex-col",
},
]
extra_actions = [
{
"url_name": "projects:project-archive",
"text": "Archive",
"needs_pk": True,
"hx_post": True,
"button_class": "btn-warning",
"display_modal": False,
"htmx_target": "content",
},
{
"url_name": "projects:project-history",
"text": "History",
"needs_pk": True,
"button_class": "btn-secondary",
"display_modal": True,
"disabled_if": "is_history_action_disabled",
"disabled_reason": "get_history_action_disabled_reason",
"refresh_list_on_modal_close": True,
"modal_box_classes": "modal-box flex max-h-[calc(100dvh-2rem)] w-11/12 max-w-5xl flex-col",
},
]
def is_history_action_disabled(self, obj, request):
return obj.archived_at is None
def get_history_action_disabled_reason(self, obj, request):
if obj.archived_at is None:
return "History is only available for archived projects."
return None
def get_owner_tooltip(self, obj, request=None):
return f"{obj.owner.email} - {obj.owner.team.name}"
def get_is_overdue_tooltip(self, obj, request=None):
return "Past due and needs follow-up" if obj.is_overdue else "On schedule"
def get_needs_attention_tooltip(self, obj, request=None):
return "Blocked project" if obj.needs_attention else "Not blocked"
def get_list_cell_link(self, obj, field_name, value, *, is_property, request=None):
if field_name == "display_status" and obj.status_report_url:
return {
"url": obj.status_report_url,
"title": "Open external status report",
"open_in": "new",
}
if field_name == "owner_card":
return {
"url": self.safe_reverse("crm:owner-detail", kwargs={"pk": obj.owner_id}),
"open_in": "modal",
"modal_box_classes": "modal-box flex max-h-[calc(100dvh-2rem)] w-11/12 max-w-5xl flex-col",
}
return None
Notes
base_template_pathis required. PowerCRUD does not ship a bundled site shell.show_record_countandshow_bulk_selection_metaare separate toggles. You can show record counts without bulk-selection prompts, or vice versa.view_titleoverrides only the visible list heading. It does not change create-button text, empty-state copy, or the model’s own verbose names.view_instructionsadds plain-text helper copy directly below the visible list heading. The content is escaped and does not accept HTML.view_helpadds collapsed plain-text screen help belowview_instructionsand above the list toolbar. Thesummaryis always visible; blank lines indetailscreate paragraphs; setdefault_open = Trueonly when the help should start expanded. Help aligns to the table width, respectsview_help_min_width, and can usecolorto apply a subtle daisyUI semantic or hex colour tint.column_help_textadds optional plain-text tooltips to specific header labels. The help trigger is a separate info icon, so sortable headers keep sorting behavior.column_alignmentslets you override list body-cell alignment for specific rendered fields or properties without changing the default heuristic for the rest of the table.list_cell_tooltip_fieldsmaps selected rendered columns to semantic list-cell tooltip hooks. Named hooks are only called for configured names that are actually visible in the current list, and returned plain text may include newline characters when the semantic cell tooltip should render on multiple lines.list_cell_link_default_open_inis optional and sets the default opening mode for list-cell links on this view. If omitted, PowerCRUD assumes"new". Use"modal"when internal drill-in links should preserve the current list context.link_fieldsis intentionally narrow. Use it for the common cases where a visible column should reverse to a named detail page or use a static external URL. Dict values accept exactly one ofview_nameorurl, plus optionalpk_attr,open_in, andmodal_box_classesfor modal links.get_list_cell_link(...)is the escape hatch for conditional or row-specific link behavior. ReturningNonefalls back tolink_fields; returningFalsesuppresses declarative linking for that cell. Hook metadata can also setopen_in = "new"oropen_in = "modal", and modal hook links can setmodal_box_classes.needs_attentionis a queryset annotation field. Its publicannotate(...)name is used directly infieldsandfilterset_fields, so it appears in list order and can filter/sort without becoming an editable model form field.list_options_enabled = Trueshows Cols while keeping every allowedfields/propertiesentry available for the current session.default_list_fieldsmakes the reset/default state narrower; leave it unset when every allowed list column should be visible by default.- Queryset annotation fields are read-only. Keep them out of
form_fields,inline_edit_fields, andbulk_fields. - Semantic list-cell tooltips take precedence over the fallback overflow tooltip for the same cell. Unconfigured cells keep the existing overflow behavior.
- Tooltip appearance is styled through CSS variables such as
--pc-tooltip-bgand--pc-tooltip-fg, not Python view parameters. PowerCRUD defaults those to neutral daisyUI tokens, and you can override them in your app CSS when you want project-level theming. - Inline-editable cells are never linked, so if a field appears in both
inline_edit_fieldsandlink_fields, PowerCRUD logs a warning and silently keeps inline editing authoritative. form_classis the source of truth for editable inputs in this example. Because a custom form class is configured, the example intentionally does not also setform_fields.form_display_fieldsrenders model fields in a separate read-onlyContextblock above update forms. This is useful foreditable=Falsefields or other contextual data the user should see while editing.form_disabled_fieldskeeps real update-form inputs visible but disabled. PowerCRUD uses Django field disabling rather than widget-only attrs, so posted tampering is ignored and the saved instance value is preserved.- A good use case for
view_titleis when the page heading needs UX-friendly wording such asMy List of BooksorActive Client Projects, while the underlying model metadata should stay reusable elsewhere. extra_buttons_mode = "dropdown"is optional. When omitted, top-levelextra_buttonskeep the legacy visible-button behavior. Dropdown mode keeps built-in actions such as Create visible and moves only configuredextra_buttonsinto the top toolbarMoremenu.extra_actions_mode = "dropdown"is optional. When omitted,extra_actionskeep the legacy visible-button behavior. Dropdown mode keepsView/Edit/Deletevisible and moves only the extra row actions into the row-levelMoremenu.extra_actions_dropdown_open_upward_bottom_rows = 3makes theMoremenu open upward for the last three rendered rows on the current page. Set it to0if you want every dropdown to keep opening downward.uses_selection = Trueturns a header button into a selection-aware action that reads the persisted PowerCRUD selection at the endpoint.selection_min_behavior = "disable"lets the frontend grey out a selection-aware header button until enough rows are selected, but the endpoint should still validate the selection server-side.modal_box_classeson a modalextra_buttonsitem replaces the view-level modal box classes only while that button's modal is open. Keepflex max-h-[calc(100dvh-2rem)] flex-colin the string if you want the supplied viewport-bounded behavior plus a custom width.modal_box_classesalso works on list-cell links and hook-returned list-cell links whenopen_in = "modal".disabled_statelets rowextra_actionsdisable themselves through one hook that returns a disabled reason string, whiledisabled_if/disabled_reasonremain available as the legacy paired hook style.modal_box_classesworks the same way on modalextra_actions, including actions rendered inside the dropdownMoremenu.inline_edit_fieldsis the current inline-editing configuration. Olderinline_edit_enabledusage is legacy and should not be used in new code.inline_edit_always_visible = Trueis the current default, so editable cells keep a subtle resting hint unless you disable it.inline_edit_highlight_accent = "#14b8a6"is the current default accent. PowerCRUD derives the lighter resting tint and stronger hover/focus tint from that single hex value.field_queryset_dependenciesis the current declarative way to scope child select querysets in regular forms and inline editing, and to apply static queryset restrictions reused by bulk edit dropdowns.static_filtersis the static-rule companion to the dynamicdepends_on/filter_byshape. In the example above,project_manageris always limited to active rows, whiletagsstill depend on the selectedowner.bulk_fieldsandbulk_deleteenable the synchronous bulk-edit UI. The queryset-wide bulk-selection metadata action also depends on the globalPOWERCRUD_SETTINGS["BULK_MAX_SELECTED_RECORDS"]cap.searchable_selects = Trueenables Tom Select enhancement for eligible select widgets in forms, inline editing, bulk edit forms, and filter forms.default_filterset_fields = ["owner", "status"]keeps the rest of the allowed filters available but hidden behind the built-inAdd filtercontrol on first render.filter_favourites_enabled = Trueturns on the optional saved favourites toolbar when thepowercrud.contrib.favouritesapp is installed and the sharedpowercrudURLs are mounted. Otherwise, PowerCRUD silently leaves the favourites UI disabled.- Saved favourites are optional. They require adding
powercrud.contrib.favouritestoINSTALLED_APPS, running its migrations, mountinginclude("powercrud.urls", namespace="powercrud"), and following the setup described in the advanced guide.
For focused explanations of individual options, use the dedicated guides and the main Configuration Options reference: