django-htmx-base 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_htmx_base-0.1.2/LICENSE +21 -0
- django_htmx_base-0.1.2/PKG-INFO +214 -0
- django_htmx_base-0.1.2/README.md +186 -0
- django_htmx_base-0.1.2/django_htmx_base/__init__.py +0 -0
- django_htmx_base-0.1.2/django_htmx_base/admin.py +1 -0
- django_htmx_base-0.1.2/django_htmx_base/apps.py +7 -0
- django_htmx_base-0.1.2/django_htmx_base/forms.py +20 -0
- django_htmx_base-0.1.2/django_htmx_base/migrations/__init__.py +0 -0
- django_htmx_base-0.1.2/django_htmx_base/models.py +210 -0
- django_htmx_base-0.1.2/django_htmx_base/routers.py +157 -0
- django_htmx_base-0.1.2/django_htmx_base/tests.py +1 -0
- django_htmx_base-0.1.2/django_htmx_base/urls.py +9 -0
- django_htmx_base-0.1.2/django_htmx_base/views.py +7 -0
- django_htmx_base-0.1.2/django_htmx_base/viewsets.py +712 -0
- django_htmx_base-0.1.2/django_htmx_base/widgets.py +42 -0
- django_htmx_base-0.1.2/django_htmx_base.egg-info/PKG-INFO +214 -0
- django_htmx_base-0.1.2/django_htmx_base.egg-info/SOURCES.txt +21 -0
- django_htmx_base-0.1.2/django_htmx_base.egg-info/dependency_links.txt +1 -0
- django_htmx_base-0.1.2/django_htmx_base.egg-info/requires.txt +2 -0
- django_htmx_base-0.1.2/django_htmx_base.egg-info/top_level.txt +1 -0
- django_htmx_base-0.1.2/pyproject.toml +64 -0
- django_htmx_base-0.1.2/setup.cfg +4 -0
- django_htmx_base-0.1.2/tests/tests.py +128 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 retriever-laboratories
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-htmx-base
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A collection of classes and abstract models for django + htmx projects
|
|
5
|
+
Author-email: Jorge Guerra <jorge@retlabs.cl>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/retriever-laboratories/django-htmx-base
|
|
8
|
+
Keywords: Django,HTMX
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 6.0
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: django>=6.0.2
|
|
26
|
+
Requires-Dist: django-htmx>=1.27.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# django-htmx-base
|
|
30
|
+
|
|
31
|
+
`django-htmx-base` is a lightweight Django library that provides viewsets, formsets, automated routers, and abstract models designed to eliminate CRUD boilerplate in Django + HTMX projects.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
Before installing, ensure your environment meets the following requirements:
|
|
39
|
+
* **Python:** 3.12+
|
|
40
|
+
* **Django:** 6.0+
|
|
41
|
+
* **django-htmx:** 1.27+
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Installation & Quick Start
|
|
46
|
+
|
|
47
|
+
### 1. Installation
|
|
48
|
+
|
|
49
|
+
Install directly from GitHub via VCS (version 0.1.1):
|
|
50
|
+
|
|
51
|
+
**uv:**
|
|
52
|
+
```bash
|
|
53
|
+
uv add git+https://github.com
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**pip:**
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install git+https://github.com
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Configure Django Settings
|
|
62
|
+
|
|
63
|
+
Add the core application configuration and the required HTMX middleware to your project's `settings.py`:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
INSTALLED_APPS = [
|
|
67
|
+
# ...
|
|
68
|
+
"django_htmx",
|
|
69
|
+
"django_htmx_base",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
MIDDLEWARE = [
|
|
73
|
+
# ...
|
|
74
|
+
"django_htmx.middleware.HtmxMiddleware",
|
|
75
|
+
]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Usage Guide
|
|
81
|
+
|
|
82
|
+
### 1. Abstract Models
|
|
83
|
+
|
|
84
|
+
Inherit from `BaseModel` to quickly apply standard architecture baselines to your database layers:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# my_app/models.py
|
|
88
|
+
from django.db import models
|
|
89
|
+
from django.urls import reverse
|
|
90
|
+
from django_htmx_base.models import BaseModel
|
|
91
|
+
|
|
92
|
+
class Product(BaseModel):
|
|
93
|
+
name = models.CharField(max_length=256, unique=True)
|
|
94
|
+
description = models.TextField(blank=True)
|
|
95
|
+
|
|
96
|
+
class Meta:
|
|
97
|
+
ordering = ['-id']
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Execute your migrations normally inside your primary project environment:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python manage.py makemigrations
|
|
104
|
+
python manage.py migrate
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 2. Viewsets and Routing
|
|
108
|
+
|
|
109
|
+
Combine viewsets and routers to eliminate boilerplate URL configurations and automatically expose structural HTMX URLs to your templates.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# my_app/views.py
|
|
113
|
+
from django_htmx_base.viewsets import GenericHtmxViewSet
|
|
114
|
+
from .models import Product
|
|
115
|
+
|
|
116
|
+
class ProductViewSet(GenericHtmxViewSet):
|
|
117
|
+
"""
|
|
118
|
+
Handles standard CRUD actions and automatically provides
|
|
119
|
+
the `view.url_names` context object to your templates.
|
|
120
|
+
"""
|
|
121
|
+
model = Product
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### Registering the Viewset
|
|
125
|
+
|
|
126
|
+
Use the `HTMXRouter` to map your viewset to standard URL endpoints seamlessly:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# my_app/urls.py
|
|
130
|
+
from django.urls import path, include
|
|
131
|
+
from django_htmx_base.routers import HTMXRouter
|
|
132
|
+
from .views import ProductViewSet
|
|
133
|
+
|
|
134
|
+
router = HTMXRouter()
|
|
135
|
+
router.register(ProductViewSet)
|
|
136
|
+
|
|
137
|
+
app_name = "my_app"
|
|
138
|
+
|
|
139
|
+
urlpatterns = [
|
|
140
|
+
path("products/", include(router.urls)),
|
|
141
|
+
]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 3. Automated View Routing and Context
|
|
145
|
+
|
|
146
|
+
The viewset automatically discovers templates inside your application's `templates/[app_label]/` directory. Out of the box, it checks for a modern nested model directory first and falls back to a flat hyphenated filename.
|
|
147
|
+
|
|
148
|
+
To enable generic, model-agnostic app fallbacks (e.g., sharing one `list.html` across multiple models), you must explicitly set `use_app_templates = True` on your viewset.
|
|
149
|
+
|
|
150
|
+
For a viewset handling a `Product` model inside a `products` app, the engine searches for templates in this order:
|
|
151
|
+
|
|
152
|
+
1. **Nested Model Folder (Default):** `templates/products/product/list.html`
|
|
153
|
+
2. **Flat Model Fallback (Default):** `templates/products/product-list.html`
|
|
154
|
+
3. **Generic App Fallback:** `templates/products/list.html` *(Requires `use_app_templates = True`)*
|
|
155
|
+
|
|
156
|
+
#### Example Viewset Configuration
|
|
157
|
+
```python
|
|
158
|
+
class ProductViewSet(GenericHtmxViewSet):
|
|
159
|
+
model = Product
|
|
160
|
+
|
|
161
|
+
# Optional: Enable the generic app-level fallback template
|
|
162
|
+
use_app_templates = True
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Expose standard execution paths seamlessly to your user interface templates via integrated property-driven attributes:
|
|
166
|
+
|
|
167
|
+
```html
|
|
168
|
+
<!-- Generic Template Table Row Actions Component -->
|
|
169
|
+
{% for obj in object_list %}
|
|
170
|
+
<tr>
|
|
171
|
+
<td>{{ obj.name }}</td>
|
|
172
|
+
<td>
|
|
173
|
+
<!-- Dynamically resolves view.basename + your htmx action settings safely -->
|
|
174
|
+
<button hx-get="{% url view.url_names.edit pk=obj.pk %}" hx-target="#modal">Edit</button>
|
|
175
|
+
<button hx-delete="{% url view.url_names.delete pk=obj.pk %}" hx-confirm="Are you sure?">Delete</button>
|
|
176
|
+
</td>
|
|
177
|
+
</tr>
|
|
178
|
+
{% endfor %}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Local Development & Contribution
|
|
184
|
+
|
|
185
|
+
If you want to modify this library, run its independent isolated test sandbox, or contribute fixes, set up your standalone repository workspace:
|
|
186
|
+
|
|
187
|
+
### 1. Setup Environment
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Clone the repository
|
|
191
|
+
git clone https://github.com
|
|
192
|
+
cd django-htmx-base
|
|
193
|
+
|
|
194
|
+
# Create and activate a standard virtual environment
|
|
195
|
+
python -m venv .venv
|
|
196
|
+
source .venv/bin/activate # On Windows use: .venv\Scripts\activate
|
|
197
|
+
|
|
198
|
+
# Install the package in editable mode with test suites dependencies
|
|
199
|
+
pip install -e .[dev]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 2. Running the Test Suite
|
|
203
|
+
|
|
204
|
+
Execute the local test suite runner directly via standard python commands:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
python runtests.py
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
If you use the **uv** package manager, you can bypass explicit manual virtual environment setups entirely and execute everything inside an isolated wrapper on the fly:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
uv run python runtests.py
|
|
214
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# django-htmx-base
|
|
2
|
+
|
|
3
|
+
`django-htmx-base` is a lightweight Django library that provides viewsets, formsets, automated routers, and abstract models designed to eliminate CRUD boilerplate in Django + HTMX projects.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
Before installing, ensure your environment meets the following requirements:
|
|
11
|
+
* **Python:** 3.12+
|
|
12
|
+
* **Django:** 6.0+
|
|
13
|
+
* **django-htmx:** 1.27+
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation & Quick Start
|
|
18
|
+
|
|
19
|
+
### 1. Installation
|
|
20
|
+
|
|
21
|
+
Install directly from GitHub via VCS (version 0.1.1):
|
|
22
|
+
|
|
23
|
+
**uv:**
|
|
24
|
+
```bash
|
|
25
|
+
uv add git+https://github.com
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**pip:**
|
|
29
|
+
```bash
|
|
30
|
+
python -m pip install git+https://github.com
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Configure Django Settings
|
|
34
|
+
|
|
35
|
+
Add the core application configuration and the required HTMX middleware to your project's `settings.py`:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
INSTALLED_APPS = [
|
|
39
|
+
# ...
|
|
40
|
+
"django_htmx",
|
|
41
|
+
"django_htmx_base",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
MIDDLEWARE = [
|
|
45
|
+
# ...
|
|
46
|
+
"django_htmx.middleware.HtmxMiddleware",
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Usage Guide
|
|
53
|
+
|
|
54
|
+
### 1. Abstract Models
|
|
55
|
+
|
|
56
|
+
Inherit from `BaseModel` to quickly apply standard architecture baselines to your database layers:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# my_app/models.py
|
|
60
|
+
from django.db import models
|
|
61
|
+
from django.urls import reverse
|
|
62
|
+
from django_htmx_base.models import BaseModel
|
|
63
|
+
|
|
64
|
+
class Product(BaseModel):
|
|
65
|
+
name = models.CharField(max_length=256, unique=True)
|
|
66
|
+
description = models.TextField(blank=True)
|
|
67
|
+
|
|
68
|
+
class Meta:
|
|
69
|
+
ordering = ['-id']
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Execute your migrations normally inside your primary project environment:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python manage.py makemigrations
|
|
76
|
+
python manage.py migrate
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Viewsets and Routing
|
|
80
|
+
|
|
81
|
+
Combine viewsets and routers to eliminate boilerplate URL configurations and automatically expose structural HTMX URLs to your templates.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# my_app/views.py
|
|
85
|
+
from django_htmx_base.viewsets import GenericHtmxViewSet
|
|
86
|
+
from .models import Product
|
|
87
|
+
|
|
88
|
+
class ProductViewSet(GenericHtmxViewSet):
|
|
89
|
+
"""
|
|
90
|
+
Handles standard CRUD actions and automatically provides
|
|
91
|
+
the `view.url_names` context object to your templates.
|
|
92
|
+
"""
|
|
93
|
+
model = Product
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Registering the Viewset
|
|
97
|
+
|
|
98
|
+
Use the `HTMXRouter` to map your viewset to standard URL endpoints seamlessly:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# my_app/urls.py
|
|
102
|
+
from django.urls import path, include
|
|
103
|
+
from django_htmx_base.routers import HTMXRouter
|
|
104
|
+
from .views import ProductViewSet
|
|
105
|
+
|
|
106
|
+
router = HTMXRouter()
|
|
107
|
+
router.register(ProductViewSet)
|
|
108
|
+
|
|
109
|
+
app_name = "my_app"
|
|
110
|
+
|
|
111
|
+
urlpatterns = [
|
|
112
|
+
path("products/", include(router.urls)),
|
|
113
|
+
]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3. Automated View Routing and Context
|
|
117
|
+
|
|
118
|
+
The viewset automatically discovers templates inside your application's `templates/[app_label]/` directory. Out of the box, it checks for a modern nested model directory first and falls back to a flat hyphenated filename.
|
|
119
|
+
|
|
120
|
+
To enable generic, model-agnostic app fallbacks (e.g., sharing one `list.html` across multiple models), you must explicitly set `use_app_templates = True` on your viewset.
|
|
121
|
+
|
|
122
|
+
For a viewset handling a `Product` model inside a `products` app, the engine searches for templates in this order:
|
|
123
|
+
|
|
124
|
+
1. **Nested Model Folder (Default):** `templates/products/product/list.html`
|
|
125
|
+
2. **Flat Model Fallback (Default):** `templates/products/product-list.html`
|
|
126
|
+
3. **Generic App Fallback:** `templates/products/list.html` *(Requires `use_app_templates = True`)*
|
|
127
|
+
|
|
128
|
+
#### Example Viewset Configuration
|
|
129
|
+
```python
|
|
130
|
+
class ProductViewSet(GenericHtmxViewSet):
|
|
131
|
+
model = Product
|
|
132
|
+
|
|
133
|
+
# Optional: Enable the generic app-level fallback template
|
|
134
|
+
use_app_templates = True
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Expose standard execution paths seamlessly to your user interface templates via integrated property-driven attributes:
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<!-- Generic Template Table Row Actions Component -->
|
|
141
|
+
{% for obj in object_list %}
|
|
142
|
+
<tr>
|
|
143
|
+
<td>{{ obj.name }}</td>
|
|
144
|
+
<td>
|
|
145
|
+
<!-- Dynamically resolves view.basename + your htmx action settings safely -->
|
|
146
|
+
<button hx-get="{% url view.url_names.edit pk=obj.pk %}" hx-target="#modal">Edit</button>
|
|
147
|
+
<button hx-delete="{% url view.url_names.delete pk=obj.pk %}" hx-confirm="Are you sure?">Delete</button>
|
|
148
|
+
</td>
|
|
149
|
+
</tr>
|
|
150
|
+
{% endfor %}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Local Development & Contribution
|
|
156
|
+
|
|
157
|
+
If you want to modify this library, run its independent isolated test sandbox, or contribute fixes, set up your standalone repository workspace:
|
|
158
|
+
|
|
159
|
+
### 1. Setup Environment
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Clone the repository
|
|
163
|
+
git clone https://github.com
|
|
164
|
+
cd django-htmx-base
|
|
165
|
+
|
|
166
|
+
# Create and activate a standard virtual environment
|
|
167
|
+
python -m venv .venv
|
|
168
|
+
source .venv/bin/activate # On Windows use: .venv\Scripts\activate
|
|
169
|
+
|
|
170
|
+
# Install the package in editable mode with test suites dependencies
|
|
171
|
+
pip install -e .[dev]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 2. Running the Test Suite
|
|
175
|
+
|
|
176
|
+
Execute the local test suite runner directly via standard python commands:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python runtests.py
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
If you use the **uv** package manager, you can bypass explicit manual virtual environment setups entirely and execute everything inside an isolated wrapper on the fly:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
uv run python runtests.py
|
|
186
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Register your models here.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# django
|
|
2
|
+
from django import forms
|
|
3
|
+
|
|
4
|
+
# widgets
|
|
5
|
+
from django_htmx_base.widgets import WidgetStylerMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseForm(WidgetStylerMixin, forms.Form):
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
self.apply_widget_styles()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseModelForm(WidgetStylerMixin, forms.ModelForm):
|
|
15
|
+
class Meta:
|
|
16
|
+
fields = "__all__"
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
self.apply_widget_styles()
|
|
File without changes
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# standard
|
|
2
|
+
import csv
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from io import StringIO
|
|
5
|
+
|
|
6
|
+
# django
|
|
7
|
+
from django.db import models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FilterInputType(StrEnum):
|
|
11
|
+
CHECKBOX = "checkbox"
|
|
12
|
+
DATE = "date"
|
|
13
|
+
DATETIME_LOCAL = "datetime-local"
|
|
14
|
+
MULTISELECT = "multiselect"
|
|
15
|
+
NUMBER = "number"
|
|
16
|
+
RADIO = "radio"
|
|
17
|
+
RANGE = "range"
|
|
18
|
+
SELECT = "select"
|
|
19
|
+
TEXT = "text"
|
|
20
|
+
TIME = "time"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def BaseField(base_field_class, **kwargs): # noqa: N802
|
|
24
|
+
sortable = kwargs.pop("sortable", False)
|
|
25
|
+
partial = kwargs.pop("partial", None)
|
|
26
|
+
css_class = kwargs.pop("css_class", "")
|
|
27
|
+
filtrable = kwargs.pop("filtrable", False)
|
|
28
|
+
filter_input_type = FilterInputType(
|
|
29
|
+
kwargs.pop("filter_input_type", FilterInputType.TEXT)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
field = base_field_class(**kwargs)
|
|
33
|
+
field.sortable = sortable
|
|
34
|
+
field.partial = partial
|
|
35
|
+
field.css_class = css_class
|
|
36
|
+
field.filtrable = filtrable
|
|
37
|
+
field.filter_input_type = filter_input_type
|
|
38
|
+
|
|
39
|
+
return field
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BaseModel(models.Model):
|
|
43
|
+
"""
|
|
44
|
+
Abstract base for all models
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# fields
|
|
48
|
+
created_at = BaseField(
|
|
49
|
+
models.DateTimeField, auto_now_add=True, editable=False, sortable=True
|
|
50
|
+
)
|
|
51
|
+
updated_at = BaseField(models.DateTimeField, auto_now=True, editable=False)
|
|
52
|
+
is_active = BaseField(
|
|
53
|
+
models.BooleanField,
|
|
54
|
+
default=True,
|
|
55
|
+
filtrable=True,
|
|
56
|
+
filter_input_type=FilterInputType.SELECT,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# view attributes
|
|
60
|
+
_display_fields = ["id", "created_at", "is_active"]
|
|
61
|
+
_downloadable = True
|
|
62
|
+
|
|
63
|
+
class Meta:
|
|
64
|
+
abstract = True
|
|
65
|
+
ordering = ("-created_at",)
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
return f"{self.__class__.__name__}(id={getattr(self, 'id', None)})"
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_filtrable_fields(cls):
|
|
72
|
+
"""
|
|
73
|
+
Returns context-ready filter metadata for each filtrable field.
|
|
74
|
+
"""
|
|
75
|
+
return [
|
|
76
|
+
field
|
|
77
|
+
for field in cls.get_display_fields()
|
|
78
|
+
if getattr(field, "filtrable", False)
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def get_filters_objects(cls):
|
|
83
|
+
filters = []
|
|
84
|
+
|
|
85
|
+
for field in cls.get_filtrable_fields():
|
|
86
|
+
filter_config = {
|
|
87
|
+
"field": field.name,
|
|
88
|
+
"filter_input_type": getattr(
|
|
89
|
+
field, "filter_input_type", FilterInputType.TEXT
|
|
90
|
+
).value,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if hasattr(field, "choices") and field.choices:
|
|
94
|
+
filter_config["options"] = [
|
|
95
|
+
{"value": value, "label": label}
|
|
96
|
+
for value, label in field.choices
|
|
97
|
+
if value not in (None, "")
|
|
98
|
+
]
|
|
99
|
+
elif isinstance(field, models.BooleanField):
|
|
100
|
+
filter_config["options"] = [
|
|
101
|
+
{"value": "True", "label": "True"},
|
|
102
|
+
{"value": "False", "label": "False"},
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
filters.append(filter_config)
|
|
106
|
+
|
|
107
|
+
return filters
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def filters(self):
|
|
111
|
+
return self.get_filtrable_fields()
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def filters_objects(self):
|
|
115
|
+
return self.get_filters_objects()
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def sortable_fields(cls):
|
|
119
|
+
"""
|
|
120
|
+
Returns a list of fields that are sortable.
|
|
121
|
+
"""
|
|
122
|
+
return [
|
|
123
|
+
field.name
|
|
124
|
+
for field in cls.get_display_fields()
|
|
125
|
+
if getattr(field, "sortable", False)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def get_display_fields(cls):
|
|
130
|
+
return [
|
|
131
|
+
field
|
|
132
|
+
for field in cls._meta.get_fields()
|
|
133
|
+
if field.name in cls._display_fields
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def display_fields(self):
|
|
138
|
+
return self.get_display_fields()
|
|
139
|
+
|
|
140
|
+
def as_list(self):
|
|
141
|
+
"""Ordered values for this instance, matching ``display_fields`` order."""
|
|
142
|
+
values = []
|
|
143
|
+
for field in self.display_fields:
|
|
144
|
+
if getattr(field, "choices", None):
|
|
145
|
+
value = getattr(self, f"get_{field.name}_display")()
|
|
146
|
+
else:
|
|
147
|
+
value = getattr(self, field.name)
|
|
148
|
+
|
|
149
|
+
values.append(value)
|
|
150
|
+
|
|
151
|
+
return values
|
|
152
|
+
|
|
153
|
+
def as_field_values_objects_list(self):
|
|
154
|
+
"""Ordered cells for this instance, matching ``display_fields`` order."""
|
|
155
|
+
field_objects = []
|
|
156
|
+
|
|
157
|
+
for field in self.display_fields:
|
|
158
|
+
if getattr(field, "choices", None):
|
|
159
|
+
value = getattr(self, f"get_{field.name}_display")()
|
|
160
|
+
else:
|
|
161
|
+
value = getattr(self, field.name)
|
|
162
|
+
|
|
163
|
+
field_objects.append(
|
|
164
|
+
{
|
|
165
|
+
"field": field.name,
|
|
166
|
+
"value": value,
|
|
167
|
+
"partial": getattr(field, "partial", None),
|
|
168
|
+
"class": getattr(field, "css_class", ""),
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return field_objects
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def to_csv(cls, queryset):
|
|
176
|
+
output = StringIO(newline="")
|
|
177
|
+
writer = csv.writer(output)
|
|
178
|
+
writer.writerow(field.name for field in cls.get_display_fields())
|
|
179
|
+
|
|
180
|
+
for object in queryset:
|
|
181
|
+
writer.writerow(object.as_list())
|
|
182
|
+
|
|
183
|
+
return output.getvalue()
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def as_field_names_objects_list(cls):
|
|
187
|
+
columns = []
|
|
188
|
+
|
|
189
|
+
for field in cls.get_display_fields():
|
|
190
|
+
column = {
|
|
191
|
+
"field": field.name,
|
|
192
|
+
"sortable": getattr(field, "sortable", False),
|
|
193
|
+
"filtrable": getattr(field, "filtrable", False),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
partial = getattr(field, "partial", None)
|
|
197
|
+
if partial:
|
|
198
|
+
column["partial"] = partial
|
|
199
|
+
|
|
200
|
+
css_class = getattr(field, "css_class", None)
|
|
201
|
+
if css_class:
|
|
202
|
+
column["class"] = css_class
|
|
203
|
+
|
|
204
|
+
columns.append(column)
|
|
205
|
+
|
|
206
|
+
return columns
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def is_downloadable(cls):
|
|
210
|
+
return cls._downloadable
|