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",
}
default_datetime_value_format = "datetime"
column_value_formats = {
"updated_at": "time",
"completed_at": "datetime",
}
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_presentation": {"size": "extra_wide"},
},
"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_presentation = {"size": "wide"}
bulk_modal_presentation = {"size": "extra_wide"}
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
page_size_options = [10, 25, 50]
page_size_all_enabled = False
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.",
"permission_check": "can_view_project_report",
"permission_behavior": "hide",
"refresh_list_on_modal_close": True,
"modal_presentation": {"size": "extra_wide"},
},
]
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",
"permission_check": "can_archive_project",
"permission_behavior": "hide",
},
{
"url_name": "projects:project-history",
"text": "History",
"needs_pk": True,
"button_class": "btn-secondary",
"display_modal": True,
"permission_check": "can_view_project_history",
"permission_behavior": "hide",
"hidden_if": "should_hide_history_action",
"disabled_state": "get_history_action_disabled_state",
"refresh_list_on_modal_close": True,
"modal_presentation": {"size": "extra_wide"},
},
]
def should_hide_history_action(self, obj, request):
return obj.status == "draft"
def can_view_project_report(self, request, obj=None):
return request.user.has_perm("projects.view_project_report")
def can_archive_project(self, request, obj=None):
return request.user.has_perm("projects.archive_project")
def can_view_project_history(self, request, obj=None):
return request.user.has_perm("projects.view_project_history")
def has_power_create_permission(self, request):
return request.user.has_perm("projects.add_project")
def has_power_detail_permission(self, request, obj):
return request.user.has_perm("projects.view_project")
def has_power_update_permission(self, request, obj):
return request.user.has_perm("projects.change_project")
def has_power_delete_permission(self, request, obj):
return request.user.has_perm("projects.delete_project")
def has_power_bulk_update_permission(self, request):
return request.user.has_perm("projects.change_project")
def has_power_bulk_delete_permission(self, request):
return request.user.has_perm("projects.delete_project")
def get_history_action_disabled_state(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_presentation": {"size": "extra_wide"},
}
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 pack-native 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.default_datetime_value_formatcontrols the unconfigured list display ofDateTimeFieldcolumns on this view; package default isdatefor legacy compatibility.column_value_formatsthen overrides individual typed temporal fields or annotations; values aredate,time, ordatetime. Set DjangoDATE_FORMAT,TIME_FORMAT, andDATETIME_FORMATinsettings.pyto control their rendered appearance.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_presentationfor 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_presentation.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.has_power_create_permission(),has_power_detail_permission(),has_power_update_permission(),has_power_delete_permission(),has_power_bulk_update_permission(), andhas_power_bulk_delete_permission()hide and deny PowerCRUD-owned operations when the user lacks permission.permission_checkonextra_buttonsandextra_actionscontrols whether custom affordances render before row-state, selection-state, or disabled-state hooks run.uses_selection = Trueturns a header button into a selection-aware action that reads the persisted PowerCRUD selection at the endpoint. It can render selector controls even when the view does not configure built-in bulk edit/delete.- Selection-aware header buttons clear the persisted selection by default after a successful HTMX request completes. Set
clear_selection_on_success = Falsefor summary or preview modals that should preserve the user's selection. extra_button_selection_controls_disabled = Trueis for cases where the button uses selected rows, but this list should not show checkboxes just because of that button.- This is mainly useful when the selected rows come from somewhere else, or when the page has its own custom way to choose rows. Bulk edit and bulk delete still show checkboxes because they need them.
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_presentationon a modalextra_buttonsitem merges with the view presentation only while that button's modal is open.modal_presentationalso works on list-cell links and hook-returned list-cell links whenopen_in = "modal".hidden_iflets rowextra_actionsomit themselves entirely when an action is not applicable to that row.disabled_statelets rowextra_actionsdisable themselves through one hook that returns a disabled reason string. The legacydisabled_if/disabled_reasonpair remains available for compatibility but is deprecated.- Custom action and button endpoints should still enforce permissions server-side. PowerCRUD only enforces backend permission hooks for its own create, detail, update, delete, and inline-update flows.
modal_presentationworks 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: