Compare commits
4 Commits
422ffb3284
...
1d0420975d
| Author | SHA1 | Date | |
|---|---|---|---|
|
1d0420975d
|
|||
|
ce16efccb9
|
|||
|
9f2f5f11c5
|
|||
|
048b98fc58
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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*
|
||||||
2
LICENSE
2
LICENSE
@@ -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
415
README.md
@@ -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 **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.
|
||||||
|
|||||||
@@ -145,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"]
|
||||||
@@ -327,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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user