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.
@@ -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,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class BaseConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "django_htmx_base"
7
+ label = "base"
@@ -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()
@@ -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