Conditionally Disabling an Action
cv_action_enabled is a secondary gate that runs after cv_has_access (permission) has
already passed. The two hooks answer different questions:
| Hook | Question |
|---|---|
cv_has_access |
"May this user perform this action at all?" (permission) |
cv_action_enabled |
"Is this action currently applicable to this object?" (state) |
A typical use case: a "locked" group that prevents members from being added or removed, even though the user would normally have permission to do so.
Default and Override Signature
@classmethod
def cv_action_enabled(cls, user, obj=None) -> bool:
return True # always enabled by default
Override on any view class to add state-based conditions:
class PersonDeleteView(DeleteViewPermissionRequired):
cv_viewset = cv_person
@classmethod
def cv_action_enabled(cls, user, obj=None):
return not (obj and obj.group.filter(locked=True).exists())
Behavior When False
When cv_action_enabled returns False:
- The action's button is hidden entirely (not greyed out) in list actions, context actions, and card actions.
- A direct GET or POST to the action's URL returns 403 for an authenticated user (anonymous users receive a login redirect, as usual).
The obj Parameter
obj is the model instance the action concerns. Its value depends on the view type:
| View type | obj value |
|---|---|
| Object views (detail, update, delete, action, custom form) | The model instance being acted on |
| Child create view | The parent instance (since no child instance exists yet) |
| Top-level create with no parent | None |
The helper cv_get_action_object() resolves this: it returns self.get_object() for object
views and self.cv_get_parent_object() for child-create views.
Example: Locked Group Disables Member Add/Remove
class PersonDeleteView(CrispyModelViewMixin, MessageMixin, DeleteViewPermissionRequired):
form_class = CrispyDeleteForm
cv_viewset = cv_person
cv_message = "Deleted person »{object}«"
@classmethod
def cv_action_enabled(cls, user, obj=None):
# obj is the Person row; members of a locked group cannot be removed.
return not (obj and obj.group.filter(locked=True).exists())
class PersonCreateView(CrispyModelViewMixin, MessageMixin, CreateViewParentMixin, CreateViewPermissionRequired):
cv_viewset = cv_person
@classmethod
def cv_action_enabled(cls, user, obj=None):
# obj is the parent Group; a locked group cannot gain members.
return not (obj and obj.locked)
See examples/bootstrap5/app/views/group_members.py for the full working example.
Enforcement Points
cv_action_enabled is enforced at two layers — button rendering and request dispatch:
Button hiding — all action templates (list_action.html, context_action.html,
card_action.html) guard their output with {% if cv_action_enabled is not False %}.
The cv_action_enabled flag is injected into the template context by the rendering
infrastructure.
Request enforcement (plain views) — CrudViewPermissionRequiredMixin.has_permission()
calls cv_get_action_object() then cv_action_enabled(). Returning False causes
PermissionDenied (HTTP 403) for the authenticated user.
Request enforcement (guardian views) — two mixins handle the guardian path:
GuardianObjectPermissionMixin.get_object()— for object actions (detail, update, delete, action views). After the per-object guardian check, it callscv_action_enabled()and raisesPermissionDeniedif it returnsFalse.GuardianParentPermissionMixin.dispatch()— for child-create views. After checking the parent permission, it callscv_action_enabled()with the resolved parent and raisesPermissionDeniedif it returnsFalse.
See Also
- ListView —
cv_list_actionscontrols which per-row action buttons are shown - DeleteView —
cv_check_delete_protection()for form-level delete blocking - Per-Object Permissions —
cv_has_accessand guardian integration