# πŸš€ 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`: ```shell git+https://git.zm.is/XF/django-crud-mixins ``` Or install via pip: ```shell pip install git+https://git.zm.is/XF/django-crud-mixins ``` ## πŸ”§ Usage ### πŸ“‚ Import the CRUD mixins ```python from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectRead, ObjectUpdate ``` ### πŸ“ Define CRUD Views ```python 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` ```python path("accounts//", accounts.AccountList.as_view(), name="accounts"), path( "accounts//create/", accounts.AccountCreate.as_view(), name="account_create", ), path( "accounts//update//", accounts.AccountUpdate.as_view(), name="account_update", ), path( "accounts//delete//", 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 `
` section of each view: ```html {% for button in extra_buttons %} {% 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 ```python 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: ```python 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`: ```python 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`: ```python 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 ```python 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 1. **Filtering:** The `IdentifierPermissionMixin` ensures only identifiers belonging to a specific `Person` (and owned by the user) are retrieved. 2. **View Permissions:** The `set_extra_args` method filters data by enforcing **user ownership** and **parent object constraints**. 3. **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. 4. **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//identifiers//` | Lists identifiers for a `Person`. | | `/person//identifiers/create/` | Creates a new identifier under a `Person`. | | `/person//identifiers/update///` | Updates a specific identifier. | | `/person//identifiers/delete///` | Deletes an identifier for a `Person`. | ### View ```python 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**: ```html ``` ## πŸš€ Example Templates ### πŸ”₯ List Template (partials/account-list.html) ```html {% for account in object_list %} {% empty %} {% endfor %}
Account Email Actions
{{ account.name }} {{ account.email }} Edit Delete
No accounts found.
``` ## ⚠️ 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.