Lightweight CRUD mixins for Django CBVs with user-based filtering, pagination, and HTMX support for dynamic UI updates. 🚀
Go to file
2025-02-07 22:46:08 +00:00
mixins Add post function to ObjectRead and ObjectList 2025-02-07 22:17:50 +00:00
.gitignore Update gitignore 2025-02-07 22:15:14 +00:00
.pre-commit-config.yaml Add templates 2023-02-10 07:20:30 +00:00
LICENSE Update LICENSE copyright holder and year 2025-02-07 22:15:28 +00:00
MANIFEST.in Add files to manifest properly 2023-02-10 20:18:49 +00:00
pyproject.toml Remove commented code in config files 2023-02-10 20:31:14 +00:00
README.md Vastly improve documentation 2025-02-07 22:46:08 +00:00
setup.cfg Remove commented code in config files 2023-02-10 20:31:14 +00:00

🚀 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

  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/<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 Persons 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>

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.