django-dynamic-admin-forms 3.2.10__py3-none-any.whl

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,3 @@
1
+ """Add simple dynamic interaction to the otherwise static django admin."""
2
+
3
+ __version__ = "3.2.10"
@@ -0,0 +1,76 @@
1
+ import json
2
+ from itertools import filterfalse
3
+
4
+ from django.apps import apps
5
+ from django.contrib import admin
6
+ from django.core.exceptions import FieldDoesNotExist, PermissionDenied
7
+ from django.http import HttpResponse
8
+
9
+
10
+ class DynamicModelAdminMixin:
11
+ dynamic_fields = ()
12
+ dynamic_select_fields = None
13
+ dynamic_input_fields = None
14
+
15
+ def get_form(self, request, obj=None, change=False, **kwargs):
16
+ form = super().get_form(request, obj, **{"change": change, **kwargs})
17
+ self.dynamic_select_fields = list(filter(self._is_related_field, self.dynamic_fields))
18
+ self.dynamic_input_fields = list(filterfalse(self._is_related_field, self.dynamic_fields))
19
+ return form
20
+
21
+ def _is_related_field(self, field_name):
22
+ try:
23
+ return self.opts.get_field(field_name).is_relation
24
+ except FieldDoesNotExist:
25
+ return False
26
+
27
+ @staticmethod
28
+ def render_field(request) -> HttpResponse:
29
+ app_label = request.GET.get("app_label")
30
+ model_name = request.GET.get("model_name")
31
+ field_names = request.GET.getlist("field_names")
32
+
33
+ if not (app_label and model_name and field_names):
34
+ return HttpResponse("Invalid arguments", status=400)
35
+
36
+ model = apps.get_model(app_label, model_name)
37
+
38
+ # instantiate model_admin form from request data and get field
39
+ model_admin = admin.site._registry[model]
40
+
41
+ # check permissions
42
+ if not model_admin.has_module_permission(request):
43
+ raise PermissionDenied
44
+
45
+ model_form = model_admin.get_form(request)
46
+ bound_form = model_form(request.POST)
47
+
48
+ snippets = []
49
+ for field_name in field_names:
50
+ bound_field = bound_form[field_name]
51
+
52
+ # save custom queryset in field
53
+ hidden = False
54
+ method_name = f"get_dynamic_{field_name}_field"
55
+ if hasattr(model_admin, method_name):
56
+ method = getattr(model_admin, method_name)
57
+ bound_form.full_clean()
58
+ queryset, value, hidden = method(bound_form.cleaned_data)
59
+
60
+ bound_field.field.queryset = queryset
61
+ bound_field.form.data = bound_field.form.data.copy()
62
+ bound_field.form.data[field_name] = value
63
+
64
+ skip_update = field_name in request.FILES and not hidden
65
+ html = bound_field.as_widget()
66
+
67
+ snippets.append(
68
+ {
69
+ "field_name": field_name,
70
+ "html": html,
71
+ "hidden": hidden,
72
+ "skipUpdate": skip_update,
73
+ }
74
+ )
75
+
76
+ return HttpResponse(json.dumps(snippets), status=200)
@@ -0,0 +1,5 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DynamicSelectConfig(AppConfig):
5
+ name = "django_dynamic_admin_forms"
@@ -0,0 +1,72 @@
1
+ var DynamicAdmin = {
2
+ //# sourceURL="dynamic_admin.js"
3
+ handleResponse: function (target, data) {
4
+ var $ = django.jQuery;
5
+
6
+ // set hidden class in parent form-row (or form-group if using jazzmin)
7
+ if (data.hidden) {
8
+ $(target.closest(".form-row,.form-group")).addClass("hidden");
9
+ } else {
10
+ $(target.closest(".form-row,.form-group")).removeClass("hidden");
11
+ }
12
+
13
+ // file fields will have this skipUpdate flag set
14
+ // TODO It might be easier to just look at the type attribute of the input element...
15
+ if (data.skipUpdate) {
16
+ return;
17
+ }
18
+
19
+ // Update the options for the select widget (either select2 instance or normal select element)
20
+ if ($(target).find("select").hasClass("select2-hidden-accessible")) {
21
+ var select2Widget = $(target).find("select");
22
+ var currentVal = select2Widget.val();
23
+ var options = $($.parseHTML(data.html)).find("option");
24
+ select2Widget.find("option").remove();
25
+ select2Widget.append(options);
26
+ select2Widget.val(currentVal);
27
+ } else {
28
+ target.outerHTML = data.html;
29
+ }
30
+ },
31
+
32
+ dynamicWidgets: function (
33
+ app_label,
34
+ model_name,
35
+ select_field_names,
36
+ input_field_names
37
+ ) {
38
+ var $ = django.jQuery;
39
+ var that = this;
40
+
41
+ var $form = $("#" + model_name + "_form");
42
+
43
+ $form.on("change", function () {
44
+ var field_names = [...select_field_names, ...input_field_names];
45
+ var params = new URLSearchParams([
46
+ ["app_label", app_label],
47
+ ["model_name", model_name],
48
+ ...field_names.map((name) => ["field_names", name]),
49
+ ]);
50
+ var url = `/dynamic-admin-form/?${params}`;
51
+ $.post({
52
+ url,
53
+ data: new FormData(this),
54
+ contentType: false,
55
+ processData: false,
56
+ success: function (data) {
57
+ snippets = JSON.parse(data);
58
+ snippets.forEach(({ field_name, ...data }) => {
59
+ if (select_field_names.indexOf(field_name) >= 0) {
60
+ var target = $(
61
+ ".field-" + field_name + " .related-widget-wrapper"
62
+ )[0];
63
+ } else {
64
+ var target = $("#id_" + field_name)[0];
65
+ }
66
+ that.handleResponse(target, data);
67
+ });
68
+ },
69
+ });
70
+ });
71
+ },
72
+ };
@@ -0,0 +1,37 @@
1
+ {% extends "admin/change_form.html" %}
2
+ {% load static %}
3
+
4
+ {% block admin_change_form_document_ready %}
5
+ {{ block.super }}
6
+ <script type="text/javascript" src="{% static 'js/dynamic_admin.js' %}"></script>
7
+
8
+ {{ adminform.model_admin.dynamic_select_fields|json_script:"dynamic_select_fields" }}
9
+ {{ adminform.model_admin.dynamic_input_fields|json_script:"dynamic_input_fields" }}
10
+
11
+ <script type="text/javascript">
12
+ var dynamic_select_fields = JSON.parse(document.getElementById('dynamic_select_fields').textContent);
13
+ var dynamic_input_fields = JSON.parse(document.getElementById('dynamic_input_fields').textContent);
14
+ DynamicAdmin.dynamicWidgets("{{ opts.app_label }}", "{{ opts.model_name }}", dynamic_select_fields, dynamic_input_fields);
15
+ </script>
16
+ {% endblock admin_change_form_document_ready %}
17
+
18
+ {# Jazzmin-Support: the extrajs block is added in the jazzmin admin templates and won't do any harm for normal django admin #}
19
+ {% block extrajs %}
20
+ {{ block.super }}
21
+ <script>
22
+ // Find all select2 instances in the current change form and trigger the change event manually
23
+ // whenever the select2 instance changes it's value.
24
+ $(function() {
25
+ $('.select2-hidden-accessible').each(function (i, select) {
26
+ var $select = $(select);
27
+ $select.on('change', function(event) {
28
+ var form = $(event.target).closest('form')[0];
29
+ // for some reason, jQuery's .trigger() method doesn't seem to work here, so we trigger a change event manually
30
+ if (form) {
31
+ form.dispatchEvent(new Event("change"));
32
+ }
33
+ })
34
+ })
35
+ });
36
+ </script>
37
+ {% endblock extrajs %}
@@ -0,0 +1,8 @@
1
+ from django.contrib import admin
2
+ from django.urls import path
3
+
4
+ from django_dynamic_admin_forms.admin import DynamicModelAdminMixin
5
+
6
+ urlpatterns = [
7
+ path("", admin.site.admin_view(DynamicModelAdminMixin.render_field)),
8
+ ]
@@ -0,0 +1,278 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-dynamic-admin-forms
3
+ Version: 3.2.10
4
+ Summary: Add simple dynamic interaction to the otherwise static django admin.
5
+ Project-URL: Homepage, https://github.com/ambient-innovation/django-dynamic-admin/
6
+ Project-URL: Documentation, https://django-dynamic-admin-forms.readthedocs.io/en/latest/index.html
7
+ Project-URL: Maintained by, https://ambient.digital/
8
+ Project-URL: Bugtracker, https://github.com/ambient-innovation/django-dynamic-admin/issues
9
+ Project-URL: Changelog, https://django-dynamic-admin-forms.readthedocs.io/en/latest/features/changelog.html
10
+ Author-email: Ambient Digital <hello@ambient.digital>, Fabian Binz <fabian.binz@ambient.digital>
11
+ License: MIT License
12
+
13
+ Copyright (c) 2022 Ambient Innovation: GmbH
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE.md
33
+ Classifier: Development Status :: 5 - Production/Stable
34
+ Classifier: Environment :: Web Environment
35
+ Classifier: Framework :: Django
36
+ Classifier: Framework :: Django :: 4.2
37
+ Classifier: Framework :: Django :: 5.1
38
+ Classifier: Framework :: Django :: 5.2
39
+ Classifier: Intended Audience :: Developers
40
+ Classifier: License :: OSI Approved :: MIT License
41
+ Classifier: Natural Language :: English
42
+ Classifier: Operating System :: OS Independent
43
+ Classifier: Programming Language :: Python
44
+ Classifier: Programming Language :: Python :: 3
45
+ Classifier: Programming Language :: Python :: 3.10
46
+ Classifier: Programming Language :: Python :: 3.11
47
+ Classifier: Programming Language :: Python :: 3.12
48
+ Classifier: Programming Language :: Python :: 3.13
49
+ Classifier: Programming Language :: Python :: 3.14
50
+ Classifier: Topic :: Utilities
51
+ Requires-Python: >=3.10
52
+ Requires-Dist: django>=4.2
53
+ Description-Content-Type: text/markdown
54
+
55
+ [![PyPI release](https://img.shields.io/pypi/v/django-dynamic-admin-forms.svg)](https://pypi.org/project/django-dynamic-admin-forms/)
56
+ [![Downloads](https://static.pepy.tech/badge/django-dynamic-admin-forms)](https://pepy.tech/project/django-dynamic-admin-forms)
57
+ [![Coverage](https://img.shields.io/badge/Coverage-100.0%25-success)](https://github.com/ambient-innovation/django-dynamic-admin/actions?workflow=CI)
58
+ [![Linting](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
59
+ [![Coding Style](https://img.shields.io/badge/code%20style-Ruff-000000.svg)](https://github.com/astral-sh/ruff)
60
+ [![Documentation Status](https://readthedocs.org/projects/django-dynamic-admin-forms/badge/?version=latest)](https://django-dynamic-admin-forms.readthedocs.io/en/latest/?badge=latest)
61
+
62
+ Add simple interactions to the otherwise static django admin.
63
+
64
+ [PyPI](https://pypi.org/project/django-dynamic-admin-forms/) | [GitHub](https://github.com/ambient-innovation/django-dynamic-admin) | [Full documentation](https://django-dynamic-admin-forms.readthedocs.io/en/latest/index.html)
65
+
66
+ Creator & Maintainer: [Ambient Digital](https://ambient.digital/)
67
+
68
+ # django-dynamic-admin-forms
69
+
70
+ Add simple interactions to the otherwise static django admin.
71
+
72
+ [![demo.gif](https://i.postimg.cc/YCf7LV2m/demo.gif)](https://postimg.cc/Yv9ZJdWp)
73
+
74
+ ## Installation
75
+
76
+ - Install the package via pip:
77
+
78
+ ```pip install django-dynamic-admin-forms```
79
+
80
+ or via pipenv:
81
+
82
+ ```pipenv install django-dynamic-admin-forms```
83
+ - Add the module to `INSTALLED_APPS`:
84
+ ```python
85
+ INSTALLED_APPS = (
86
+ "django_dynamic_admin_forms",
87
+ "django.contrib.admin",
88
+ )
89
+ ```
90
+ Ensure that the `dynamic_admin_forms` comes before the
91
+ default `django.contrib.admin` in the list of installed apps,
92
+ because otherwise the templates, which are overwritten by `dynamic_admin_forms`
93
+ won't be found.
94
+ - Ensure that the `dynamic_admin_forms` templates are found via using `APP_DIRS` setting:
95
+ ```python
96
+ TEMPLATES = [
97
+ {
98
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
99
+ "APP_DIRS": True,
100
+ },
101
+ ]
102
+ ```
103
+ - Run `python manage.py collectstatic` to include this apps Javascript code in your `settings.STATIC_ROOT` directory
104
+
105
+ ## Usage
106
+ - Add the `django_dynamic_admin_forms.DynamicModelAdminMixin` to your admin classes
107
+ - Add the `django_dynamic_admin_forms.urls` to your urls
108
+ ```python
109
+ from django.contrib import admin
110
+ from django.urls import path, include
111
+
112
+ urlpatterns = [
113
+ path("admin/", admin.site.urls),
114
+ path("dynamic-admin-form/", include("django_dynamic_admin_forms.urls")),
115
+ ]
116
+ ```
117
+ - In addition to the standard `fields` declaration, specify a list of `dynamic_fields`
118
+ - For each dynamic field, add a method `get_dynamic_{field_name}_field` to the admin
119
+ - Input: `data: Dict[str, Any]` - the cleaned form data
120
+ - Output:
121
+ - `queryset: Optional[Queryset]` - The values to select from
122
+ - `value: Any` - The value, the field should have (must be compatible to the field type)
123
+ - `hidden: Bool` - True, if field should be hidden
124
+
125
+ - A rather non-sensical example:
126
+ ```python
127
+ from django.contrib import admin
128
+
129
+ from .models import MyModel
130
+ from django_dynamic_admin_forms.admin import DynamicModelAdminMixin
131
+
132
+
133
+ @admin.register(MyModel)
134
+ class MyModelAdmin(DynamicModelAdminMixin, admin.ModelAdmin):
135
+ fields = ("name", "city")
136
+ dynamic_fields = ("city",)
137
+
138
+ def get_dynamic_city_field(self, data):
139
+ # automatically choose first city that matches first letter of name
140
+ name = data.get("name")
141
+ if not name:
142
+ queryset = City.objects.all()
143
+ value = data.get("city")
144
+ else:
145
+ queryset = City.objects.filter(name__startswith=name[0])
146
+ value = queryset.first()
147
+ hidden = not queryset.exists()
148
+ return queryset, value, hidden
149
+ ```
150
+
151
+ ## How it works
152
+ Whenever a dynamic form changes, an event handler makes a request to a special endpoint, which returns new HTML to swap
153
+ into the existing form. This new HTML is directly generated by `django.contrib.admin`, so we only have to set the
154
+ outerHTML of the correct HTML elements to update the form.
155
+
156
+ ## Limitations
157
+ - does not work in conjunction with inlines
158
+ - does not validate that the selected value is really part of the original queryset
159
+ - if anybody can modify your DOM, they could potentially inject invalid values
160
+ - you have to write `Model.clean()` methods to guard against that
161
+ - only tested with Django 3.2
162
+
163
+ ## Development
164
+
165
+ For local development, create a virtual environment
166
+ in the `testproj` folder:
167
+ ```shell
168
+ $ cd testproj
169
+ $ python3 -m venv .venv
170
+ $ source .venv/bin/activate
171
+ $ cd ..
172
+ $ flit install --symlink
173
+ ```
174
+ Now the package should be available in your virtual environment
175
+ and any changes should be directly visible.
176
+
177
+ Alternatively, copy the directory `dynamic_admin_forms`
178
+ into any normal django project, so that the python interpreter
179
+ finds the local version instead of the installed (old) version.
180
+
181
+ ## Running E2E tests
182
+
183
+ To run end-to-end tests locally:
184
+ ```shell
185
+ $ cd testproj
186
+ $ python manage.py runserver 0.0.0.0:8000 & # start server
187
+ $ python manage.py loaddata fixtures/fixtures-dev.json
188
+ $ cd ../e2e
189
+ $ yarn install # or npm install (only needed first time)
190
+ $ yarn cypress # or npm run cypress
191
+ ```
192
+
193
+ ## Installation
194
+
195
+ - Install the package via pip:
196
+
197
+ `pip install django-dynamic-admin-forms`
198
+
199
+ or via pipenv:
200
+
201
+ `pipenv install django-dynamic-admin-forms`
202
+
203
+ - Add the module to `INSTALLED_APPS`:
204
+ ```python
205
+ INSTALLED_APPS = (
206
+ "django_dynamic_admin_forms",
207
+ "django.contrib.admin",
208
+ )
209
+ ```
210
+ Ensure that the `dynamic_admin_forms` comes before the
211
+ default `django.contrib.admin` in the list of installed apps,
212
+ because otherwise the templates, which are overwritten by `dynamic_admin_forms`
213
+ won't be found.
214
+
215
+ - Ensure that the `dynamic_admin_forms` templates are found via using `APP_DIRS` setting:
216
+ ```python
217
+ TEMPLATES = [
218
+ {
219
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
220
+ "APP_DIRS": True,
221
+ },
222
+ ]
223
+ ```
224
+
225
+ - Run `python manage.py collectstatic` to include this apps Javascript code in your `settings.STATIC_ROOT` directory
226
+
227
+ ### Publish to ReadTheDocs.io
228
+
229
+ - Fetch the latest changes in GitHub mirror and push them
230
+ - Trigger new build at ReadTheDocs.io (follow instructions in admin panel at RTD) if the GitHub webhook is not yet set
231
+ up.
232
+
233
+ ### Preparation and building
234
+
235
+ This package uses [uv](https://github.com/astral-sh/uv) for dependency management and building.
236
+
237
+ - Update documentation about new/changed functionality
238
+
239
+ - Update the `CHANGES.md`
240
+
241
+ - Increment version in main `__init__.py`
242
+
243
+ - Create pull request / merge to "main"
244
+
245
+ - This project uses uv to publish to PyPI. This will create distribution files in the `dist/` directory.
246
+
247
+ ```bash
248
+ uv build
249
+ ```
250
+
251
+ ### Publishing to PyPI
252
+
253
+ To publish to the production PyPI:
254
+
255
+ ```bash
256
+ uv publish
257
+ ```
258
+
259
+ To publish to TestPyPI first (recommended for testing):
260
+
261
+ ```bash
262
+ uv publish --publish-url https://test.pypi.org/legacy/
263
+ ```
264
+
265
+ You can then test the installation from TestPyPI:
266
+
267
+ ```bash
268
+ uv pip install --index-url https://test.pypi.org/simple/ ambient-package-update
269
+ ```
270
+
271
+ ### Maintenance
272
+
273
+ Please note that this package supports the [ambient-package-update](https://pypi.org/project/ambient-package-update/).
274
+ So you don't have to worry about the maintenance of this package. This updater is rendering all important
275
+ configuration and setup files. It works similar to well-known updaters like `pyupgrade` or `django-upgrade`.
276
+
277
+ To run an update, refer to the [documentation page](https://pypi.org/project/ambient-package-update/)
278
+ of the "ambient-package-update".
@@ -0,0 +1,10 @@
1
+ django_dynamic_admin_forms/__init__.py,sha256=ktLW1f20qQrB8XgbeYT607gD8qFXqO1aO7VyvrzEr_A,102
2
+ django_dynamic_admin_forms/admin.py,sha256=1wER87eAw6byNpKCjmXRY0iyDCXndCiqWuNziwrsLXI,2770
3
+ django_dynamic_admin_forms/apps.py,sha256=PRyfkFEI8Vc1RK8tMO4-RUvkO3lxEYKSGPlDNUUXBUo,114
4
+ django_dynamic_admin_forms/urls.py,sha256=SSdld9CyKpUAP9vTi_yS9y8sSh-S1YAnhPqn4ebXFas,232
5
+ django_dynamic_admin_forms/static/js/dynamic_admin.js,sha256=8TqoxGna-pw_SEIsBQ87gwCKe0Vo5X_SfA58NiOBhyo,2258
6
+ django_dynamic_admin_forms/templates/admin/change_form.html,sha256=itcHMYmSZswZeDHDj5fNfPHQquPouQkptjy8VO-TI7k,1680
7
+ django_dynamic_admin_forms-3.2.10.dist-info/METADATA,sha256=fOroOFLVgiGdYv4e-vHvuvmxdSv2IsvZaLC1_f0dadM,10674
8
+ django_dynamic_admin_forms-3.2.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ django_dynamic_admin_forms-3.2.10.dist-info/licenses/LICENSE.md,sha256=llryfe8olgEjTmcDQJzppeGeEWRJLS8Xf_2I_gEKYQ4,1102
10
+ django_dynamic_admin_forms-3.2.10.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Ambient Innovation: GmbH
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.