Compare commits

..

19 Commits

Author SHA1 Message Date
1d0420975d Vastly improve documentation 2025-02-07 22:46:08 +00:00
ce16efccb9 Add post function to ObjectRead and ObjectList 2025-02-07 22:17:50 +00:00
9f2f5f11c5 Update LICENSE copyright holder and year 2025-02-07 22:15:28 +00:00
048b98fc58 Update gitignore 2025-02-07 22:15:14 +00:00
422ffb3284 Set kwargs on ObjectRead 2023-03-18 11:10:12 +00:00
f10400b3c4 Implement configurable HTMX method 2023-03-09 12:25:29 +00:00
96dc21019b Add extra button functionality to list and detail views 2023-03-09 12:08:21 +00:00
3d7c3504aa Remove caching from common object templates 2023-02-11 17:59:31 +00:00
502556e7c3 Don't cache object forms 2023-02-11 17:33:13 +00:00
d808ceebac Pass the right argument to queryset_mutate 2023-02-11 16:44:07 +00:00
04d41f7c97 Allow mutating the queryset 2023-02-11 16:38:22 +00:00
bb5b473898 Cache more templates 2023-02-11 14:54:57 +00:00
eba4142960 Vary objects cache on user ID and object list 2023-02-11 14:29:15 +00:00
b7b0f1e01c Cache object list 2023-02-11 14:10:18 +00:00
6579a86e44 Adjust template path in modal 2023-02-10 20:47:23 +00:00
99945c9742 Use proper template name in generic detail view 2023-02-10 20:40:29 +00:00
9d445e2d1a Use templates relative to mixins 2023-02-10 20:38:51 +00:00
f3087dba09 Remove commented code in config files 2023-02-10 20:31:14 +00:00
b9d38bdf47 Rewrite setup.cfg 2023-02-10 20:29:24 +00:00
14 changed files with 518 additions and 174 deletions

1
.gitignore vendored
View File

@@ -160,3 +160,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
*_ref*

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) <year> <copyright holders> Copyright (c) 2025 Mark Veidemanis
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

415
README.md
View File

@@ -1,27 +1,56 @@
# django-crud-mixins # 🚀 django-crud-mixins
CRUD and form mixins for Django. **Reusable Django CRUD mixins** for rapid development of single-page applications (SPA) and grid-based UIs.
Useful for single-page-applications using Gridstack. Designed for **HTMX**, **Gridstack.js**, and **Django ORM** with built-in **access control** and **permissions**.
# Usage ## 📌 Features
Add to your `requirements.txt` file: - **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 git+https://git.zm.is/XF/django-crud-mixins
``` ```
## View helpers Or install via pip:
The view helpers help create simple CRUD views for your application. They are geared towards a single-page application using Gridstack, but can be used on full pages as well by specifying a `type` of `page` always. ```shell
pip install git+https://git.zm.is/XF/django-crud-mixins
The view permission helpers add a `user=request.user` argument to all queryset filters, ensuring your views can only access the data of the requesting user. Additional filters can be set by overriding the `set_extra_args` method (detailed below).
Import the helpers from your view:
``` ```
## 🔧 Usage
### 📂 Import the CRUD mixins
```python
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectRead, ObjectUpdate from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectRead, ObjectUpdate
``` ```
Then, add the views: ### 📝 Define CRUD Views
```python ```python
class AccountList(LoginRequiredMixin, OTPRequiredMixin, ObjectList): class AccountList(LoginRequiredMixin, ObjectList):
list_template = "partials/account-list.html" list_template = "partials/account-list.html"
model = Account model = Account
page_title = "List of accounts" page_title = "List of accounts"
@@ -32,73 +61,333 @@ class AccountList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
submit_url_name = "account_create" submit_url_name = "account_create"
class AccountCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate): class AccountCreate(LoginRequiredMixin, ObjectCreate):
model = Account model = Account
form_class = AccountForm form_class = AccountForm
submit_url_name = "account_create" submit_url_name = "account_create"
class AccountUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate): class AccountUpdate(LoginRequiredMixin, ObjectUpdate):
model = Account model = Account
form_class = AccountForm form_class = AccountForm
submit_url_name = "account_update" submit_url_name = "account_update"
class AccountDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete): class AccountDelete(LoginRequiredMixin, ObjectDelete):
model = Account model = Account
``` ```
### Variables ### Add to `urls.py`
These variables can be added to the classes to adjust functionality:
Basic:
* `list_template`: the name of the template for the list view
* `model`: the model you want to view with this helper
* `page_title`: the page title to render
* `page_subtitle`: the page subtitle to render
List URLs:
* `list_url_name`: the name of the list URL to include in the context, passed as `list_url`
* `list_url_args`: arguments for the above, taken from view kwargs, or locals
Submit URLs:
* `submit_url_name`: the name of the submit URL to include in the context, passed as `submit_url` -- used in the form
* `submit_url_args`: arguments for the above, taken from view kwargs, or locals
For `ObjectList` only:
* `delete_all_url_name`: the name of the delete-all URL to include in the context, passed as `delete_all_url` -- used in the form's "Delete all" button
* `widget_options`: options for the Gristack widget
For `ObjectCreate` and `ObjectUpdate` only:
* `hide_cancel`: whether to hide the cancel button in the form
For `ObjectUpdate` only:
* `pk_required`: whether the primary key `pk` is required in the URL kwargs
### Methods
These methods can be added to the classes to adjust functionality:
For `ObjectCreate` and `ObjectUpdate` only:
* `post_save(self, obj)`: called after the object has been saved
For `ObjectCreate` only:
* `pre_save_mutate(self, user, obj)`: called before the object is saved, AbortSave can be raised with an error message to abort the save
These methods can be used on all classes, as they are inherited from the `RestrictedViewMixin`:
* `set_extra_args(self, user)`: adjusts the queryset filter with extra parameters, set `self.extra_permission_args` from this method to a dictionary of arguments
## Form permission helper
The form permission helper `RestrictedFormMixin` can be used as a mixin in your forms:
```python ```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 YourModelForm(RestrictedFormMixin, ModelForm):
class Meta: class Meta:
model = YourModel model = YourModel
fields = ( fields = ["name"]
"name",
)
``` ```
It's that simple!
The form will automatically add a `user=request.user` argument to all queryset filters, ensuring your forms can only access the data of the requesting user. 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 **Persons 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.

View File

@@ -1,7 +1,11 @@
{% load pretty %} {% load pretty %}
{% include 'partials/notify.html' %}
{% if live is not None %} {% load cache %}
{% cache 600 generic_detail request.user.id object live %}
{% include 'mixins/partials/notify.html' %}
{% if live is not None %}
<h1 class="title">Live {{ context_object_name_singular }} info</h1> <h1 class="title">Live {{ context_object_name_singular }} info</h1>
<table class="table is-fullwidth is-hoverable"> <table class="table is-fullwidth is-hoverable">
<thead> <thead>
@@ -34,9 +38,9 @@
{% endblock %} {% endblock %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% if object is not None %} {% if object is not None %}
<h1 class="title">{{ title_singular }} info</h1> <h1 class="title">{{ title_singular }} info</h1>
<table class="table is-fullwidth is-hoverable"> <table class="table is-fullwidth is-hoverable">
<thead> <thead>
@@ -69,4 +73,5 @@
{% endblock %} {% endblock %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% endcache %}

View File

@@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
{% if page_title is not None %} {% if page_title is not None %}
<h1 class="title is-4">{{ page_title }}</h1> <h1 class="title is-4">{{ page_title }}</h1>
{% endif %} {% endif %}
@@ -24,11 +24,3 @@
{% endif %} {% endif %}
<button type="submit" class="button modal-close-button">Submit</button> <button type="submit" class="button modal-close-button">Submit</button>
</form> </form>

View File

@@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
{% if page_title is not None %} {% if page_title is not None %}
<h1 class="title is-4">{{ page_title }}</h1> <h1 class="title is-4">{{ page_title }}</h1>
{% endif %} {% endif %}
@@ -39,7 +39,23 @@
</span> </span>
</button> </button>
{% endif %} {% endif %}
{% 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 %}
</div> </div>
{% include detail_template %} {% include detail_template %}

View File

@@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
{% if page_title is not None %} {% if page_title is not None %}
<h1 class="title is-4">{{ page_title }}</h1> <h1 class="title is-4">{{ page_title }}</h1>
{% endif %} {% endif %}
@@ -39,7 +39,23 @@
</span> </span>
</button> </button>
{% endif %} {% endif %}
{% 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 %}
</div> </div>
{% include list_template %} {% include list_template %}

View File

@@ -14,7 +14,7 @@
{% block modal_content %} {% block modal_content %}
{% include window_content %} {% include window_content %}
{% endblock %} {% endblock %}
{% include 'partials/close-modal.html' %} {% include 'mixins/partials/close-modal.html' %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,7 +3,7 @@
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i> <i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
{% block close_button %} {% block close_button %}
{% include 'partials/close-window.html' %} {% include 'mixins/partials/close-window.html' %}
{% endblock %} {% endblock %}
{% block heading %} {% block heading %}
{% endblock %} {% endblock %}

View File

@@ -6,7 +6,7 @@
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i> <i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
{% block close_button %} {% block close_button %}
{% include 'partials/close-widget.html' %} {% include 'mixins/partials/close-widget.html' %}
{% endblock %} {% endblock %}
<i <i
class="fa-solid fa-arrows-minimize has-text-grey-light float-right" class="fa-solid fa-arrows-minimize has-text-grey-light float-right"

View File

@@ -1,5 +1,5 @@
<magnet-block attract-distance="10" align-to="outer|center" class="floating-window"> <magnet-block attract-distance="10" align-to="outer|center" class="floating-window">
{% extends 'wm/panel.html' %} {% extends 'mixins/wm/panel.html' %}
{% block heading %} {% block heading %}
{{ title }} {{ title }}
{% endblock %} {% endblock %}

View File

@@ -50,6 +50,11 @@ class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView):
delete_all_url_name = None delete_all_url_name = None
widget_options = None widget_options = None
extra_buttons = None
def queryset_mutate(self, queryset):
pass
# copied from BaseListView # copied from BaseListView
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
type = kwargs.get("type", None) type = kwargs.get("type", None)
@@ -60,10 +65,13 @@ class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView):
self.request = request self.request = request
self.object_list = self.get_queryset(**kwargs) self.object_list = self.get_queryset(**kwargs)
if isinstance(self.object_list, HttpResponse): if isinstance(self.object_list, HttpResponse):
return self.object_list return self.object_list
if isinstance(self.object_list, HttpResponseBadRequest): if isinstance(self.object_list, HttpResponseBadRequest):
return self.object_list return self.object_list
self.queryset_mutate(self.object_list)
allow_empty = self.get_allow_empty() allow_empty = self.get_allow_empty()
self.template_name = f"mixins/wm/{type}.html" self.template_name = f"mixins/wm/{type}.html"
@@ -124,6 +132,8 @@ class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView):
context["delete_all_url"] = reverse(self.delete_all_url_name) context["delete_all_url"] = reverse(self.delete_all_url_name)
if self.widget_options: if self.widget_options:
context["widget_options"] = self.widget_options context["widget_options"] = self.widget_options
if self.extra_buttons is not None:
context["extra_buttons"] = self.extra_buttons
# Return partials for HTMX # Return partials for HTMX
if self.request.htmx: if self.request.htmx:
@@ -135,6 +145,10 @@ class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView):
context["window_content"] = self.list_template context["window_content"] = self.list_template
return self.render_to_response(context) return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.request = request
return super().get(request, *args, **kwargs)
class ObjectCreate(RestrictedViewMixin, ObjectNameMixin, CreateView): class ObjectCreate(RestrictedViewMixin, ObjectNameMixin, CreateView):
allowed_types = ["modal", "widget", "window", "page"] allowed_types = ["modal", "widget", "window", "page"]
@@ -255,6 +269,8 @@ class ObjectRead(RestrictedViewMixin, ObjectNameMixin, DetailView):
request = None request = None
extra_buttons = None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
type = kwargs.get("type", None) type = kwargs.get("type", None)
if not type: if not type:
@@ -272,6 +288,7 @@ class ObjectRead(RestrictedViewMixin, ObjectNameMixin, DetailView):
detail_url_args[arg] = kwargs[arg] detail_url_args[arg] = kwargs[arg]
self.request = request self.request = request
self.kwargs = kwargs
self.object = self.get_object(**kwargs) self.object = self.get_object(**kwargs)
if isinstance(self.object, HttpResponse): if isinstance(self.object, HttpResponse):
return self.object return self.object
@@ -300,6 +317,9 @@ class ObjectRead(RestrictedViewMixin, ObjectNameMixin, DetailView):
self.detail_url_name, kwargs=detail_url_args self.detail_url_name, kwargs=detail_url_args
) )
if self.extra_buttons is not None:
context["extra_buttons"] = self.extra_buttons
# Return partials for HTMX # Return partials for HTMX
if self.request.htmx: if self.request.htmx:
if request.headers["HX-Target"] == self.context_object_name + "-info": if request.headers["HX-Target"] == self.context_object_name + "-info":
@@ -311,6 +331,10 @@ class ObjectRead(RestrictedViewMixin, ObjectNameMixin, DetailView):
return self.render_to_response(context) return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.request = request
return super().get(request, *args, **kwargs)
class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView): class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView):
allowed_types = ["modal", "widget", "window", "page"] allowed_types = ["modal", "widget", "window", "page"]

View File

@@ -3,27 +3,3 @@
[build-system] [build-system]
requires = ["setuptools", "wheel"] requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project]
name = "django-crud-mixins"
version = "1.0.2"
description = "CRUD mixins for Django class-based views"
readme = "README.md"
authors = [{ name = "Mark Veidemanis", email = "m@zm.is" }]
license = { file = "LICENSE" }
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Framework :: Django :: 4.1",
"Intended Audience :: Developers",
]
keywords = ["django", "mixins", "helpers", "crud"]
dependencies = [
"django",
"django-rest-framework",
]
requires-python = ">=3.9"
[project.urls]
Homepage = "https://git.zm.is/XF/django-crud-mixins"

View File

@@ -1,3 +1,28 @@
[metadata]
name = django-crud-mixins
version = 1.0.3
author = Mark Veidemanis
author_email = m@zm.is
url = https://git.zm.is/XF/django-crud-mixins
description = CRUD mixins for Django class-based views.
long_description = file: README.md
long_description_content_type = text/markdown
keywords = django, mixins, helpers, crud
license = MIT
classifiers =
License :: OSI Approved :: BSD License
Programming Language :: Python :: 3
Framework :: Django :: 4.1
Intended Audience :: Developers
[options] [options]
packages = find: packages = find:
zip_safe = True
include_package_data = True include_package_data = True
install_requires =
django
django-rest-framework
[options.package_data]
mixins = templates/mixins/*, README.md
* = README.md