# 🚀 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.example.invalid/vendor/django-crud-mixins
```

Or install via pip:
```shell
pip install git+https://git.example.invalid/vendor/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/<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:
```html
{% 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
```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/<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

```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
<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)
```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.
