mixins | ||
.gitignore | ||
.pre-commit-config.yaml | ||
LICENSE | ||
MANIFEST.in | ||
pyproject.toml | ||
README.md | ||
setup.cfg |
🚀 django-crud-mixins
Reusable Django CRUD mixins for rapid development of single-page applications (SPA) and grid-based UIs.
Designed for HTMX, Gridstack.js, and Django ORM with built-in access control and permissions.
📌 Features
- Django 4+ support with modular mixins
- HTMX-friendly partials
- Pre-configured Gridstack.js widget support
- RBAC enforcement with per-user filtering
- Reusable mixins for List, Create, Read, Update, Delete
- Automatic permission enforcement on querysets
- Seamless integration with Django Forms and QuerySets
- Prevents unauthorized data access
- Extra Buttons for dynamic action-based UI extensions
- Context-aware detail views with Pretty Print formatting
📑 CRUD Mixins Overview
Mixin | Description |
---|---|
ObjectList |
List view with pagination and permission-based filtering |
ObjectCreate |
Create new objects with form validation |
ObjectRead |
Read-only detail view with pre-fetching |
ObjectUpdate |
Edit existing objects with permission enforcement |
ObjectDelete |
Soft-delete or hard-delete objects securely |
RestrictedViewMixin |
Enforces user-based filtering on querysets |
RestrictedFormMixin |
Auto-filters form choices based on user permissions |
⚙️ Installation
Add to your requirements.txt
:
git+https://git.zm.is/XF/django-crud-mixins
Or install via pip:
pip install git+https://git.zm.is/XF/django-crud-mixins
🔧 Usage
📂 Import the CRUD mixins
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectRead, ObjectUpdate
📝 Define CRUD Views
class AccountList(LoginRequiredMixin, ObjectList):
list_template = "partials/account-list.html"
model = Account
page_title = "List of accounts"
list_url_name = "accounts"
list_url_args = ["type"]
submit_url_name = "account_create"
class AccountCreate(LoginRequiredMixin, ObjectCreate):
model = Account
form_class = AccountForm
submit_url_name = "account_create"
class AccountUpdate(LoginRequiredMixin, ObjectUpdate):
model = Account
form_class = AccountForm
submit_url_name = "account_update"
class AccountDelete(LoginRequiredMixin, ObjectDelete):
model = Account
Add to urls.py
path("accounts/<str:type>/", accounts.AccountList.as_view(), name="accounts"),
path(
"accounts/<str:type>/create/",
accounts.AccountCreate.as_view(),
name="account_create",
),
path(
"accounts/<str:type>/update/<str:pk>/",
accounts.AccountUpdate.as_view(),
name="account_update",
),
path(
"accounts/<str:type>/delete/<str:pk>/",
accounts.AccountDelete.as_view(),
name="account_delete",
),
⚙️ Configuration Options
General Options
Variable | Description |
---|---|
list_template |
Template name for list view |
model |
Django model used for the view |
page_title |
Title of the page |
page_subtitle |
Subtitle of the page |
List URLs
Variable | Description |
---|---|
list_url_name |
URL name for listing objects |
list_url_args |
URL arguments for list endpoint |
Submit URLs (Forms)
Variable | Description |
---|---|
submit_url_name |
URL for submitting forms |
submit_url_args |
URL args for submitting |
ObjectList-Specific Options
Variable | Description |
---|---|
delete_all_url_name |
URL for bulk deletion |
widget_options |
Gridstack widget config |
ObjectCreate & ObjectUpdate
Variable | Description |
---|---|
hide_cancel |
Hide cancel button in forms |
ObjectUpdate-Only
Variable | Description |
---|---|
pk_required |
Enforce primary key requirement |
🔄 Extra Buttons
The mixins allow dynamic extra buttons to be added to views for custom actions.
Extra buttons are rendered inside the <div class="buttons">
section of each view:
{% for button in extra_buttons %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-{{ button.method }}="{{ button.url }}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
{% if button.confirm %}hx-confirm="Are you sure you wish to {{ button.action }}?"{% endif %}
class="button">
<span class="icon-text">
<span class="icon">
<i class="{{ button.icon }}"></i>
</span>
<span>{{ button.label }}</span>
</span>
</button>
{% endfor %}
Structure of Extra Buttons
Each extra button is a dictionary with:
Key | Description |
---|---|
method |
HTMX method (get , post , delete , etc.) |
url |
Endpoint to be triggered |
action |
Action name (for confirmation) |
icon |
CSS class for icon (FontAwesome recommended) |
label |
Display text for the button |
confirm |
Optional confirmation prompt |
Example Usage in Views
class CustomListView(ObjectList):
extra_buttons = [
{
"method": "post",
"url": reverse_lazy("custom-action"),
"icon": "fa-solid fa-bolt",
"label": "Trigger Action",
"confirm": True,
"action": "trigger this action"
}
]
🖼️ Pretty Print Context (pretty)
The Pretty Print (pretty) context is used in ObjectRead
detail views to enhance object representation.
It is loaded via {% load pretty %}
and ensures formatted output of complex data.
Implement it into your project's templatetags
folder to use:
import orjson
from django import template
register = template.Library()
@register.filter
def pretty(data):
return orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8")
When to Use Pretty Print
- Complex JSON Fields (e.g., structured responses)
- Nested Dictionaries (e.g., API outputs)
- Formatted Logs & Debugging
🔥 Extending Functionality
✅ Enforce Query Restrictions (RBAC)
Add extra permission filters via extra_permission_args
:
class CustomListView(ObjectList):
def set_extra_args(self, user):
self.extra_permission_args = {"organization": user.organization}
This ensures users can only see objects from their organization.
📍 Hook Into Save Events
Modify or abort saves via pre_save_mutate
and post_save
:
class SecureObjectCreate(ObjectCreate):
def pre_save_mutate(self, user, obj):
if not user.is_admin:
raise AbortSave("You do not have permission to save this object.")
def post_save(self, obj):
send_notification(f"Object {obj.id} was created.")
🔒 Secure Form QuerySets
from mixins.forms import RestrictedFormMixin
from django.forms import ModelForm
class YourModelForm(RestrictedFormMixin, ModelForm):
class Meta:
model = YourModel
fields = ["name"]
This ensures users only see objects they have access to in form dropdowns.
🔗 Nested Objects & Relationships
Overview
Some objects exist within a parent object. In this case, views and URLs must be structured to reflect this relationship. The example below demonstrates managing Person Identifiers, which are linked to a Person.
How It Works
- Filtering: The
IdentifierPermissionMixin
ensures only identifiers belonging to a specificPerson
(and owned by the user) are retrieved. - View Permissions: The
set_extra_args
method filters data by enforcing user ownership and parent object constraints. - Validation:
PersonIdentifierCreate
ensures an identifier cannot be added twice.PersonIdentifierUpdate
restricts edits to only valid, existing identifiers.PersonIdentifierDelete
removes identifiers that belong to the requesting user.
- URL Structure: Each identifier action (list, create, update, delete) includes the Person ID, ensuring operations are always scoped to the correct entity.
Example: Managing Person Identifiers
📌 Views
View | Description |
---|---|
PersonIdentifierList |
Displays all identifiers linked to a specific Person . |
PersonIdentifierCreate |
Adds a new identifier for a Person , preventing duplicates. |
PersonIdentifierUpdate |
Edits an existing identifier, enforcing access control. |
PersonIdentifierDelete |
Deletes an identifier, ensuring ownership validation. |
📌 URL Routing
Route | Description |
---|---|
/person/<str:type>/identifiers/<str:person>/ |
Lists identifiers for a Person . |
/person/<str:type>/identifiers/create/<str:person> |
Creates a new identifier under a Person . |
/person/<str:type>/identifiers/update/<str:person>/<str:pk>/ |
Updates a specific identifier. |
/person/<str:type>/identifiers/delete/<str:person>/<str:pk>/ |
Deletes an identifier for a Person . |
View
class IdentifierPermissionMixin:
def set_extra_args(self, user):
self.extra_permission_args = {
"person__user": user,
"person__pk": self.kwargs["person"],
}
class PersonIdentifierList(LoginRequiredMixin, IdentifierPermissionMixin, ObjectList):
list_template = "partials/identifier-list.html"
model = PersonIdentifier
page_title = "Person Identifiers"
list_url_name = "person_identifiers"
list_url_args = ["type", "person"]
submit_url_name = "person_identifier_create"
submit_url_args = ["type", "person"]
class PersonIdentifierCreate(LoginRequiredMixin, IdentifierPermissionMixin, ObjectCreate):
model = PersonIdentifier
form_class = PersonIdentifierForm
submit_url_name = "person_identifier_create"
submit_url_args = ["type", "person"]
def form_valid(self, form):
"""If the form is invalid, render the invalid form."""
try:
return super().form_valid(form)
except IntegrityError as e:
if "UNIQUE constraint failed" in str(e):
form.add_error("identifier", "Identifier rule already exists")
return self.form_invalid(form)
else:
raise e
def pre_save_mutate(self, user, obj):
try:
person = Person.objects.get(pk=self.kwargs["person"], user=user)
obj.person = person
except Person.DoesNotExist:
log.error(f"Person {self.kwargs['person']} does not exist")
raise AbortSave("person does not exist or you don't have access")
class PersonIdentifierUpdate(LoginRequiredMixin, IdentifierPermissionMixin, ObjectUpdate):
model = PersonIdentifier
form_class = PersonIdentifierForm
submit_url_name = "person_identifier_update"
submit_url_args = ["type", "pk", "person"]
class PersonIdentifierDelete(LoginRequiredMixin, IdentifierPermissionMixin, ObjectDelete):
model = PersonIdentifier
📌 Button Example (Template)
To display a button linking to a Person’s Identifiers:
<a href="{% url 'person_identifiers' type='page' person=item.id %}">
<button class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
🚀 Example Templates
🔥 List Template (partials/account-list.html)
<table class="table is-striped">
<thead>
<tr>
<th>Account</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for account in object_list %}
<tr>
<td>{{ account.name }}</td>
<td>{{ account.email }}</td>
<td>
<a href="{% url 'account_update' pk=account.pk %}" class="button is-small">Edit</a>
<a href="{% url 'account_delete' pk=account.pk %}" class="button is-danger is-small">Delete</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3">No accounts found.</td>
</tr>
{% endfor %}
</tbody>
</table>
⚠️ Legal Disclaimer
django-crud-mixins is provided "as is", without warranty of any kind. Use responsibly and comply with local data privacy laws. The maintainers are not responsible for misuse.