nilva-django-admin 0.2.0__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.
- nilva_django_admin-0.2.0/LICENSE +21 -0
- nilva_django_admin-0.2.0/PKG-INFO +175 -0
- nilva_django_admin-0.2.0/README.md +150 -0
- nilva_django_admin-0.2.0/nilva_django_admin/__init__.py +15 -0
- nilva_django_admin-0.2.0/nilva_django_admin/admin_apps.py +19 -0
- nilva_django_admin-0.2.0/nilva_django_admin/apps.py +7 -0
- nilva_django_admin-0.2.0/nilva_django_admin/base.py +175 -0
- nilva_django_admin-0.2.0/nilva_django_admin/captcha.py +159 -0
- nilva_django_admin-0.2.0/nilva_django_admin/compat.py +47 -0
- nilva_django_admin-0.2.0/nilva_django_admin/settings.py +135 -0
- nilva_django_admin-0.2.0/nilva_django_admin/sites.py +95 -0
- nilva_django_admin-0.2.0/nilva_django_admin/sso.py +186 -0
- nilva_django_admin-0.2.0/nilva_django_admin/templates/nilva_django_admin/login.html +67 -0
- nilva_django_admin-0.2.0/nilva_django_admin/urls.py +15 -0
- nilva_django_admin-0.2.0/nilva_django_admin/views.py +76 -0
- nilva_django_admin-0.2.0/nilva_django_admin.egg-info/PKG-INFO +175 -0
- nilva_django_admin-0.2.0/nilva_django_admin.egg-info/SOURCES.txt +27 -0
- nilva_django_admin-0.2.0/nilva_django_admin.egg-info/dependency_links.txt +1 -0
- nilva_django_admin-0.2.0/nilva_django_admin.egg-info/requires.txt +8 -0
- nilva_django_admin-0.2.0/nilva_django_admin.egg-info/top_level.txt +1 -0
- nilva_django_admin-0.2.0/pyproject.toml +68 -0
- nilva_django_admin-0.2.0/setup.cfg +4 -0
- nilva_django_admin-0.2.0/tests/test_base.py +71 -0
- nilva_django_admin-0.2.0/tests/test_captcha.py +135 -0
- nilva_django_admin-0.2.0/tests/test_compat.py +25 -0
- nilva_django_admin-0.2.0/tests/test_package.py +13 -0
- nilva_django_admin-0.2.0/tests/test_settings.py +42 -0
- nilva_django_admin-0.2.0/tests/test_sites.py +94 -0
- nilva_django_admin-0.2.0/tests/test_sso.py +157 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Nilva
|
|
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,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nilva-django-admin
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Nilva house Django-admin toolkit — BaseModelAdmin/BaseInlineAdmin composition, in-house Keycloak SSO admin login, captcha on login + selected models, dynamic model visibility, django-admin-interface theming
|
|
5
|
+
Author-email: Nilva <dev@nilva.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: homepage, https://gitlab.nilva.ai/nilva/dev-assets/django/nilva-django-admin
|
|
8
|
+
Project-URL: repository, https://gitlab.nilva.ai/nilva/dev-assets/django/nilva-django-admin
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 6.0
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: django>=6.0
|
|
18
|
+
Requires-Dist: django-autoutils>=2.1.1
|
|
19
|
+
Requires-Dist: django-object-actions>=5.0.0
|
|
20
|
+
Requires-Dist: requests>=2.32
|
|
21
|
+
Requires-Dist: django-admin-interface>=0.30.1
|
|
22
|
+
Provides-Extra: jalali
|
|
23
|
+
Requires-Dist: django-jalali>=7.4.0; extra == "jalali"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# nilva-django-admin
|
|
27
|
+
|
|
28
|
+
Nilva's house Django-admin toolkit: the `BaseModelAdmin` / `BaseInlineAdmin`
|
|
29
|
+
composition every backend re-declares, an **in-house Keycloak SSO admin
|
|
30
|
+
login** (no `django-admin-keycloak`), the **Cheshmeh-style captcha** on the
|
|
31
|
+
login form and on selected models' change forms, **dynamic model
|
|
32
|
+
visibility** driven by settings, the django-autoutils Django-6 compatibility
|
|
33
|
+
patch, and [django-admin-interface](https://pypi.org/project/django-admin-interface/)
|
|
34
|
+
theming as a dependency.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add nilva-django-admin # everything below
|
|
40
|
+
uv add "nilva-django-admin[jalali]" # + Persian datepickers for jDateField models
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Wiring
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# settings.py
|
|
47
|
+
INSTALLED_APPS = [
|
|
48
|
+
"admin_interface", # theming (ships with this package)
|
|
49
|
+
"colorfield",
|
|
50
|
+
...
|
|
51
|
+
"nilva_django_admin", # templates + app
|
|
52
|
+
"nilva_django_admin.admin_apps.NilvaAdminConfig", # INSTEAD of django.contrib.admin
|
|
53
|
+
...
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
NILVA_ADMIN = {
|
|
57
|
+
"APP_NAME": "shahab",
|
|
58
|
+
"SITE_TITLE": "Shahab administration",
|
|
59
|
+
"LOGIN_CAPTCHA": True,
|
|
60
|
+
"CAPTCHA_MODELS": ("user.User", "salary.*"),
|
|
61
|
+
"HIDDEN_MODELS": ("auth.Group",),
|
|
62
|
+
"KEYCLOAK": {
|
|
63
|
+
"SERVER_URL": "https://sso.nilva.ai",
|
|
64
|
+
"REALM_NAME": "nilva",
|
|
65
|
+
"CLIENT_ID": "...",
|
|
66
|
+
"CLIENT_SECRET": "...",
|
|
67
|
+
},
|
|
68
|
+
"CAPTCHA": {"BASE_URL": "https://host/services/captcha/v1.0"},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# urls.py
|
|
72
|
+
path("api/sso/", include("nilva_django_admin.urls")),
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Base admin classes
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from nilva_django_admin.base import BaseInlineAdmin, BaseModelAdmin
|
|
79
|
+
|
|
80
|
+
@admin.register(Delivery)
|
|
81
|
+
class DeliveryAdmin(BaseModelAdmin):
|
|
82
|
+
list_display = ("id", "avatar_icon", "name", "editor_icon")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
django-object-actions buttons, django-autoutils filters/avatars/edit-links/
|
|
86
|
+
pretty-JSON, `editor_icon` auditing, the duck-typed house protocols
|
|
87
|
+
(`get_obj()`, `get_prefetch_related_fields()`, `avatar_img_tag()`), Jalali
|
|
88
|
+
datepickers when `django-jalali` is installed, and the Django-6
|
|
89
|
+
`format_html` compat patch for django-autoutils 2.1.1. Defaults
|
|
90
|
+
(`LIST_PER_PAGE`, `AVATAR_ICON_FIELD`) come from `NILVA_ADMIN`.
|
|
91
|
+
|
|
92
|
+
## SSO admin login (in-house, no django-admin-keycloak)
|
|
93
|
+
|
|
94
|
+
Plain OIDC authorization-code flow implemented in
|
|
95
|
+
`nilva_django_admin.sso` / `.views`:
|
|
96
|
+
|
|
97
|
+
* `GET <mount>/login/` redirects to the realm's `/auth` endpoint (CSRF
|
|
98
|
+
`state` kept in the session, `?next=` honoured when it is same-host).
|
|
99
|
+
* `GET <mount>/callback/` exchanges the code, reads roles from the access
|
|
100
|
+
token (`realm_access` + `resource_access` + userinfo `groups`), maps
|
|
101
|
+
`{APP_NAME}_staff` → `is_staff` and `{APP_NAME}_super_user` →
|
|
102
|
+
`is_superuser` (overridable via `ROLE_STAFF_USER` / `ROLE_SUPER_USER`),
|
|
103
|
+
creates/updates the Django user and logs it in. Accounts without the
|
|
104
|
+
staff role are rejected. With `SYNC_GROUPS` (default) matching Django
|
|
105
|
+
`Group` rows are assigned.
|
|
106
|
+
* `GET <mount>/logout/` logs out of Django and the Keycloak session.
|
|
107
|
+
|
|
108
|
+
The admin login page (rendered by `NilvaAdminSite`) shows a **Login with
|
|
109
|
+
SSO** button automatically whenever `KEYCLOAK` is configured and the URLs
|
|
110
|
+
are mounted.
|
|
111
|
+
|
|
112
|
+
## Captcha (Cheshmeh's service)
|
|
113
|
+
|
|
114
|
+
`nilva_django_admin.captcha.CaptchaService` talks to the MCI/EBCOM-style
|
|
115
|
+
captcha HTTP service (`GET ?width=…` → `{id, image}`; `GET
|
|
116
|
+
/{id}/verify/{code}`). Typed codes are Persian/Arabic-digit normalized; the
|
|
117
|
+
`VALIDATION_IGNORE` staging bypass only works when `DEBUG=True`.
|
|
118
|
+
|
|
119
|
+
* **Login captcha** — `NILVA_ADMIN["LOGIN_CAPTCHA"] = True` puts the captcha
|
|
120
|
+
on the admin login form.
|
|
121
|
+
* **Model captcha** — every model matching a `CAPTCHA_MODELS` pattern gets a
|
|
122
|
+
captcha on its admin add/change form. `BaseModelAdmin` injects the field
|
|
123
|
+
and appends a CAPTCHA fieldset when the admin declares explicit
|
|
124
|
+
`fieldsets`. A failed code re-renders with a fresh challenge; the
|
|
125
|
+
challenge id travels in a hidden field, so no session state is involved.
|
|
126
|
+
|
|
127
|
+
## Dynamic model visibility
|
|
128
|
+
|
|
129
|
+
Which models the admin shows follows settings, per request — no code
|
|
130
|
+
changes or restarts of registration logic:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
NILVA_ADMIN = {
|
|
134
|
+
"VISIBLE_MODELS": ("worklog.*", "user.User"), # None → all registered
|
|
135
|
+
"HIDDEN_MODELS": ("auth.Permission",), # always wins
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`NilvaAdminSite.get_app_list` applies the `fnmatch` patterns, so the index
|
|
140
|
+
and sidebar only show matching models. And for the inverse — surfacing
|
|
141
|
+
models nobody wrote an admin for:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# urls.py, after admin.autodiscover
|
|
145
|
+
admin.site.auto_register() # every unregistered model → BaseModelAdmin
|
|
146
|
+
admin.site.auto_register(exclude=("django_celery_beat.*",))
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Theming
|
|
150
|
+
|
|
151
|
+
`django-admin-interface` is a dependency: add `admin_interface` and
|
|
152
|
+
`colorfield` to `INSTALLED_APPS` (before the admin), run `migrate`, and
|
|
153
|
+
manage themes from the admin itself.
|
|
154
|
+
|
|
155
|
+
## Settings reference — `NILVA_ADMIN`
|
|
156
|
+
|
|
157
|
+
| key | default | notes |
|
|
158
|
+
|-----|---------|-------|
|
|
159
|
+
| `APP_NAME` | `None` | role-name defaults |
|
|
160
|
+
| `LIST_PER_PAGE` / `AVATAR_ICON_FIELD` | `20` / `"avatar_32"` | `BaseModelAdmin` |
|
|
161
|
+
| `SITE_TITLE` | `None` | site header/title |
|
|
162
|
+
| `VISIBLE_MODELS` / `HIDDEN_MODELS` | `None` / `()` | fnmatch patterns |
|
|
163
|
+
| `LOGIN_CAPTCHA` | `False` | captcha on the login form |
|
|
164
|
+
| `CAPTCHA_MODELS` | `()` | captcha on these models' change forms |
|
|
165
|
+
| `CAPTCHA.BASE_URL` … | — | captcha service (`WIDTH`/`HEIGHT`/`TYPE`/`PROXY`/`TIMEOUT`/`VALIDATION_IGNORE`) |
|
|
166
|
+
| `KEYCLOAK.SERVER_URL` … | — | SSO (`REALM_NAME`/`CLIENT_ID`/`CLIENT_SECRET`/`SCOPE`/`REDIRECT_URI`/`ROLE_*`/`SYNC_GROUPS`/`VERIFY_SSL`/`TIMEOUT`) |
|
|
167
|
+
| `SSO_BUTTON_TEXT` | `"Login with SSO"` | login page button |
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
uv sync --group dev
|
|
173
|
+
./run_tests.sh # ./run_tests.sh -c for coverage
|
|
174
|
+
uv run ruff check .
|
|
175
|
+
```
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# nilva-django-admin
|
|
2
|
+
|
|
3
|
+
Nilva's house Django-admin toolkit: the `BaseModelAdmin` / `BaseInlineAdmin`
|
|
4
|
+
composition every backend re-declares, an **in-house Keycloak SSO admin
|
|
5
|
+
login** (no `django-admin-keycloak`), the **Cheshmeh-style captcha** on the
|
|
6
|
+
login form and on selected models' change forms, **dynamic model
|
|
7
|
+
visibility** driven by settings, the django-autoutils Django-6 compatibility
|
|
8
|
+
patch, and [django-admin-interface](https://pypi.org/project/django-admin-interface/)
|
|
9
|
+
theming as a dependency.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv add nilva-django-admin # everything below
|
|
15
|
+
uv add "nilva-django-admin[jalali]" # + Persian datepickers for jDateField models
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Wiring
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# settings.py
|
|
22
|
+
INSTALLED_APPS = [
|
|
23
|
+
"admin_interface", # theming (ships with this package)
|
|
24
|
+
"colorfield",
|
|
25
|
+
...
|
|
26
|
+
"nilva_django_admin", # templates + app
|
|
27
|
+
"nilva_django_admin.admin_apps.NilvaAdminConfig", # INSTEAD of django.contrib.admin
|
|
28
|
+
...
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
NILVA_ADMIN = {
|
|
32
|
+
"APP_NAME": "shahab",
|
|
33
|
+
"SITE_TITLE": "Shahab administration",
|
|
34
|
+
"LOGIN_CAPTCHA": True,
|
|
35
|
+
"CAPTCHA_MODELS": ("user.User", "salary.*"),
|
|
36
|
+
"HIDDEN_MODELS": ("auth.Group",),
|
|
37
|
+
"KEYCLOAK": {
|
|
38
|
+
"SERVER_URL": "https://sso.nilva.ai",
|
|
39
|
+
"REALM_NAME": "nilva",
|
|
40
|
+
"CLIENT_ID": "...",
|
|
41
|
+
"CLIENT_SECRET": "...",
|
|
42
|
+
},
|
|
43
|
+
"CAPTCHA": {"BASE_URL": "https://host/services/captcha/v1.0"},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# urls.py
|
|
47
|
+
path("api/sso/", include("nilva_django_admin.urls")),
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Base admin classes
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from nilva_django_admin.base import BaseInlineAdmin, BaseModelAdmin
|
|
54
|
+
|
|
55
|
+
@admin.register(Delivery)
|
|
56
|
+
class DeliveryAdmin(BaseModelAdmin):
|
|
57
|
+
list_display = ("id", "avatar_icon", "name", "editor_icon")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
django-object-actions buttons, django-autoutils filters/avatars/edit-links/
|
|
61
|
+
pretty-JSON, `editor_icon` auditing, the duck-typed house protocols
|
|
62
|
+
(`get_obj()`, `get_prefetch_related_fields()`, `avatar_img_tag()`), Jalali
|
|
63
|
+
datepickers when `django-jalali` is installed, and the Django-6
|
|
64
|
+
`format_html` compat patch for django-autoutils 2.1.1. Defaults
|
|
65
|
+
(`LIST_PER_PAGE`, `AVATAR_ICON_FIELD`) come from `NILVA_ADMIN`.
|
|
66
|
+
|
|
67
|
+
## SSO admin login (in-house, no django-admin-keycloak)
|
|
68
|
+
|
|
69
|
+
Plain OIDC authorization-code flow implemented in
|
|
70
|
+
`nilva_django_admin.sso` / `.views`:
|
|
71
|
+
|
|
72
|
+
* `GET <mount>/login/` redirects to the realm's `/auth` endpoint (CSRF
|
|
73
|
+
`state` kept in the session, `?next=` honoured when it is same-host).
|
|
74
|
+
* `GET <mount>/callback/` exchanges the code, reads roles from the access
|
|
75
|
+
token (`realm_access` + `resource_access` + userinfo `groups`), maps
|
|
76
|
+
`{APP_NAME}_staff` → `is_staff` and `{APP_NAME}_super_user` →
|
|
77
|
+
`is_superuser` (overridable via `ROLE_STAFF_USER` / `ROLE_SUPER_USER`),
|
|
78
|
+
creates/updates the Django user and logs it in. Accounts without the
|
|
79
|
+
staff role are rejected. With `SYNC_GROUPS` (default) matching Django
|
|
80
|
+
`Group` rows are assigned.
|
|
81
|
+
* `GET <mount>/logout/` logs out of Django and the Keycloak session.
|
|
82
|
+
|
|
83
|
+
The admin login page (rendered by `NilvaAdminSite`) shows a **Login with
|
|
84
|
+
SSO** button automatically whenever `KEYCLOAK` is configured and the URLs
|
|
85
|
+
are mounted.
|
|
86
|
+
|
|
87
|
+
## Captcha (Cheshmeh's service)
|
|
88
|
+
|
|
89
|
+
`nilva_django_admin.captcha.CaptchaService` talks to the MCI/EBCOM-style
|
|
90
|
+
captcha HTTP service (`GET ?width=…` → `{id, image}`; `GET
|
|
91
|
+
/{id}/verify/{code}`). Typed codes are Persian/Arabic-digit normalized; the
|
|
92
|
+
`VALIDATION_IGNORE` staging bypass only works when `DEBUG=True`.
|
|
93
|
+
|
|
94
|
+
* **Login captcha** — `NILVA_ADMIN["LOGIN_CAPTCHA"] = True` puts the captcha
|
|
95
|
+
on the admin login form.
|
|
96
|
+
* **Model captcha** — every model matching a `CAPTCHA_MODELS` pattern gets a
|
|
97
|
+
captcha on its admin add/change form. `BaseModelAdmin` injects the field
|
|
98
|
+
and appends a CAPTCHA fieldset when the admin declares explicit
|
|
99
|
+
`fieldsets`. A failed code re-renders with a fresh challenge; the
|
|
100
|
+
challenge id travels in a hidden field, so no session state is involved.
|
|
101
|
+
|
|
102
|
+
## Dynamic model visibility
|
|
103
|
+
|
|
104
|
+
Which models the admin shows follows settings, per request — no code
|
|
105
|
+
changes or restarts of registration logic:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
NILVA_ADMIN = {
|
|
109
|
+
"VISIBLE_MODELS": ("worklog.*", "user.User"), # None → all registered
|
|
110
|
+
"HIDDEN_MODELS": ("auth.Permission",), # always wins
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`NilvaAdminSite.get_app_list` applies the `fnmatch` patterns, so the index
|
|
115
|
+
and sidebar only show matching models. And for the inverse — surfacing
|
|
116
|
+
models nobody wrote an admin for:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# urls.py, after admin.autodiscover
|
|
120
|
+
admin.site.auto_register() # every unregistered model → BaseModelAdmin
|
|
121
|
+
admin.site.auto_register(exclude=("django_celery_beat.*",))
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Theming
|
|
125
|
+
|
|
126
|
+
`django-admin-interface` is a dependency: add `admin_interface` and
|
|
127
|
+
`colorfield` to `INSTALLED_APPS` (before the admin), run `migrate`, and
|
|
128
|
+
manage themes from the admin itself.
|
|
129
|
+
|
|
130
|
+
## Settings reference — `NILVA_ADMIN`
|
|
131
|
+
|
|
132
|
+
| key | default | notes |
|
|
133
|
+
|-----|---------|-------|
|
|
134
|
+
| `APP_NAME` | `None` | role-name defaults |
|
|
135
|
+
| `LIST_PER_PAGE` / `AVATAR_ICON_FIELD` | `20` / `"avatar_32"` | `BaseModelAdmin` |
|
|
136
|
+
| `SITE_TITLE` | `None` | site header/title |
|
|
137
|
+
| `VISIBLE_MODELS` / `HIDDEN_MODELS` | `None` / `()` | fnmatch patterns |
|
|
138
|
+
| `LOGIN_CAPTCHA` | `False` | captcha on the login form |
|
|
139
|
+
| `CAPTCHA_MODELS` | `()` | captcha on these models' change forms |
|
|
140
|
+
| `CAPTCHA.BASE_URL` … | — | captcha service (`WIDTH`/`HEIGHT`/`TYPE`/`PROXY`/`TIMEOUT`/`VALIDATION_IGNORE`) |
|
|
141
|
+
| `KEYCLOAK.SERVER_URL` … | — | SSO (`REALM_NAME`/`CLIENT_ID`/`CLIENT_SECRET`/`SCOPE`/`REDIRECT_URI`/`ROLE_*`/`SYNC_GROUPS`/`VERIFY_SSL`/`TIMEOUT`) |
|
|
142
|
+
| `SSO_BUTTON_TEXT` | `"Login with SSO"` | login page button |
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
uv sync --group dev
|
|
148
|
+
./run_tests.sh # ./run_tests.sh -c for coverage
|
|
149
|
+
uv run ruff check .
|
|
150
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Nilva Django Admin — the Nilva house Django-admin toolkit.
|
|
2
|
+
|
|
3
|
+
The ``BaseModelAdmin`` / ``BaseInlineAdmin`` composition every Nilva backend
|
|
4
|
+
re-declares (django-object-actions + django-autoutils avatar/filter/JSON
|
|
5
|
+
helpers + editor auditing + Jalali datepickers), the django-autoutils
|
|
6
|
+
Django-6 compatibility patch, an in-house Keycloak SSO admin login (no
|
|
7
|
+
django-admin-keycloak), the Cheshmeh-style captcha on the login form and on
|
|
8
|
+
selected models' change forms, and ``NilvaAdminSite`` with settings-driven
|
|
9
|
+
dynamic model visibility. Theming comes from ``django-admin-interface``.
|
|
10
|
+
|
|
11
|
+
Import the pieces explicitly — ``base``/``sites``/``captcha``/``sso`` need
|
|
12
|
+
Django; importing this package itself pulls nothing heavy.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""AdminConfig swap-in — lives in its own module so Django's default-AppConfig
|
|
2
|
+
detection for ``nilva_django_admin`` stays unambiguous.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.contrib.admin.apps import AdminConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NilvaAdminConfig(AdminConfig):
|
|
9
|
+
"""Use in INSTALLED_APPS *instead of* ``django.contrib.admin`` so
|
|
10
|
+
``admin.site`` becomes a :class:`nilva_django_admin.sites.NilvaAdminSite`::
|
|
11
|
+
|
|
12
|
+
INSTALLED_APPS = [
|
|
13
|
+
...
|
|
14
|
+
"nilva_django_admin",
|
|
15
|
+
"nilva_django_admin.admin_apps.NilvaAdminConfig", # not django.contrib.admin
|
|
16
|
+
]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
default_site = "nilva_django_admin.sites.NilvaAdminSite"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""House ``BaseModelAdmin`` / ``BaseInlineAdmin`` — the Shahab composition,
|
|
2
|
+
decoupled from project models via duck-typing.
|
|
3
|
+
|
|
4
|
+
Every admin class inherits from these instead of ``admin.ModelAdmin`` /
|
|
5
|
+
``admin.TabularInline`` directly. They compose django-object-actions,
|
|
6
|
+
django-autoutils (advanced filters, avatars, edit links, limited FKs, pretty
|
|
7
|
+
JSON, related inlines) and editor auditing, and honour the model protocols
|
|
8
|
+
the fleet already uses:
|
|
9
|
+
|
|
10
|
+
* ``get_obj()`` — permission-object indirection (``BASE_PERMISSION_OBJECT``)
|
|
11
|
+
* ``get_prefetch_related_fields()`` — class-level ``PREFETCH_RELATED`` lists
|
|
12
|
+
* ``avatar_img_tag(size=…)`` — avatar-model rendering
|
|
13
|
+
* ``editor`` — audit field shown via :meth:`AbstractEditorAdmin.editor_icon`
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from urllib import parse
|
|
18
|
+
|
|
19
|
+
# Importing django_jalali.admin patches Django's admin FORMFIELD_FOR_DBFIELD_DEFAULTS
|
|
20
|
+
# so jDateField/jDateTimeField render with the Persian datepicker widget. This
|
|
21
|
+
# must happen before any ModelAdmin class is defined, so it lives here in the
|
|
22
|
+
# base admin module that every app imports. Optional: skipped when
|
|
23
|
+
# django-jalali isn't installed.
|
|
24
|
+
try:
|
|
25
|
+
import django_jalali.admin # noqa: F401
|
|
26
|
+
except ImportError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
from django.contrib import admin
|
|
30
|
+
from django.db.models import QuerySet
|
|
31
|
+
from django.utils.translation import gettext_lazy as _
|
|
32
|
+
from django_autoutils.admin_utils import (
|
|
33
|
+
AdvanceListFilter,
|
|
34
|
+
AvatarAdmin,
|
|
35
|
+
EditLinkAdmin,
|
|
36
|
+
InlineRelatedAdmin,
|
|
37
|
+
LimitForeignKeyAdmin,
|
|
38
|
+
PrettyJsonAdmin,
|
|
39
|
+
admin_display,
|
|
40
|
+
)
|
|
41
|
+
from django_autoutils.html_tag import get_edit_icon
|
|
42
|
+
from django_object_actions import DjangoObjectActions
|
|
43
|
+
|
|
44
|
+
from .captcha import captcha_protected, model_requires_captcha
|
|
45
|
+
from .compat import patch_autoutils_html_tag
|
|
46
|
+
from .settings import admin_settings
|
|
47
|
+
|
|
48
|
+
patch_autoutils_html_tag()
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("nilva_django_admin")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _prefetch_for(model, queryset: QuerySet) -> QuerySet:
|
|
54
|
+
if hasattr(model, "get_prefetch_related_fields"):
|
|
55
|
+
return queryset.prefetch_related(*model.get_prefetch_related_fields())
|
|
56
|
+
return queryset
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AbstractEditorAdmin:
|
|
60
|
+
"""Mixin for models carrying an ``editor`` audit field."""
|
|
61
|
+
|
|
62
|
+
def _handle_instance(self, obj):
|
|
63
|
+
"""For change extra field from inline object call this function."""
|
|
64
|
+
|
|
65
|
+
@admin_display(description=_("editor"))
|
|
66
|
+
def editor_icon(self, obj):
|
|
67
|
+
"""Editor icon with editor name in admin panel object page."""
|
|
68
|
+
if not obj:
|
|
69
|
+
return None
|
|
70
|
+
if not hasattr(obj, "editor"):
|
|
71
|
+
return None
|
|
72
|
+
return get_edit_icon(obj.editor)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BaseModelAdmin(
|
|
76
|
+
DjangoObjectActions,
|
|
77
|
+
AdvanceListFilter,
|
|
78
|
+
AvatarAdmin,
|
|
79
|
+
AbstractEditorAdmin,
|
|
80
|
+
LimitForeignKeyAdmin,
|
|
81
|
+
PrettyJsonAdmin,
|
|
82
|
+
InlineRelatedAdmin,
|
|
83
|
+
admin.ModelAdmin,
|
|
84
|
+
):
|
|
85
|
+
"""Base admin for all models."""
|
|
86
|
+
|
|
87
|
+
search_fields = ("=id",)
|
|
88
|
+
show_full_result_count = False
|
|
89
|
+
|
|
90
|
+
def __init__(self, model, admin_site):
|
|
91
|
+
self.list_per_page = admin_settings.LIST_PER_PAGE
|
|
92
|
+
if not getattr(self, "avatar_icon_field", None):
|
|
93
|
+
self.avatar_icon_field = admin_settings.AVATAR_ICON_FIELD
|
|
94
|
+
super().__init__(model, admin_site)
|
|
95
|
+
|
|
96
|
+
def filter_queryset_by_url(self, request, queryset):
|
|
97
|
+
"""Re-apply the changelist filters/search carried in
|
|
98
|
+
``_changelist_filters`` (object-action round-trips)."""
|
|
99
|
+
changelist_filters = request.GET.get("_changelist_filters", "")
|
|
100
|
+
filters = dict(parse.parse_qsl(changelist_filters))
|
|
101
|
+
filter_fields_data = {}
|
|
102
|
+
for k, v in filters.items():
|
|
103
|
+
if k == "q" and v:
|
|
104
|
+
queryset = self.get_search_results(request, queryset, v)[0]
|
|
105
|
+
elif self.lookup_allowed(k, v, request):
|
|
106
|
+
filter_fields_data[k] = v
|
|
107
|
+
return queryset.filter(**filter_fields_data)
|
|
108
|
+
|
|
109
|
+
def _get_avatar_obj(self, obj):
|
|
110
|
+
return self.get_obj(obj)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def get_obj(obj):
|
|
114
|
+
"""Follow the ``get_obj()`` permission-object protocol when present."""
|
|
115
|
+
if hasattr(obj, "get_obj"):
|
|
116
|
+
return obj.get_obj()
|
|
117
|
+
return obj
|
|
118
|
+
|
|
119
|
+
@admin_display(description=_("icon"))
|
|
120
|
+
def avatar_icon(self, obj=None):
|
|
121
|
+
"""Render the avatar via the model's ``avatar_img_tag`` when it has
|
|
122
|
+
one, else fall back to django-autoutils' implementation."""
|
|
123
|
+
target = self._get_avatar_obj(obj) if obj is not None else None
|
|
124
|
+
if hasattr(target, "avatar_img_tag"):
|
|
125
|
+
return target.avatar_img_tag(size=30)
|
|
126
|
+
return super().avatar_icon(obj=obj)
|
|
127
|
+
|
|
128
|
+
def get_queryset(self, request):
|
|
129
|
+
return _prefetch_for(self.model, super().get_queryset(request))
|
|
130
|
+
|
|
131
|
+
def get_form(self, request, obj=None, **kwargs):
|
|
132
|
+
if model_requires_captcha(self.model):
|
|
133
|
+
# inject BEFORE the factory runs so the declared captcha fields
|
|
134
|
+
# legitimise their names in the derived Meta.fields list
|
|
135
|
+
kwargs["form"] = captcha_protected(kwargs.get("form", self.form))
|
|
136
|
+
return super().get_form(request, obj, **kwargs)
|
|
137
|
+
|
|
138
|
+
def get_fieldsets(self, request, obj=None):
|
|
139
|
+
fieldsets = super().get_fieldsets(request, obj)
|
|
140
|
+
if model_requires_captcha(self.model):
|
|
141
|
+
from django.contrib.admin.utils import flatten_fieldsets
|
|
142
|
+
|
|
143
|
+
if "captcha_code" not in flatten_fieldsets(fieldsets):
|
|
144
|
+
fieldsets = [*fieldsets, (_("CAPTCHA"), {"fields": ("captcha_id", "captcha_code")})]
|
|
145
|
+
return fieldsets
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def get_back_url_for_changelist(request):
|
|
149
|
+
return "/".join(request.get_full_path().split("/")[:-3])
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def get_back_url(request):
|
|
153
|
+
full_path = request.get_full_path()
|
|
154
|
+
if "actions" not in full_path:
|
|
155
|
+
return full_path
|
|
156
|
+
return "/".join(full_path.split("/")[:-3])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class BaseInlineAdmin(AvatarAdmin, EditLinkAdmin, LimitForeignKeyAdmin, AbstractEditorAdmin, admin.TabularInline):
|
|
160
|
+
"""Base admin for all inlines."""
|
|
161
|
+
|
|
162
|
+
extra = 0
|
|
163
|
+
|
|
164
|
+
def __init__(self, parent_model, admin_site):
|
|
165
|
+
if not getattr(self, "avatar_icon_field", None):
|
|
166
|
+
self.avatar_icon_field = admin_settings.AVATAR_ICON_FIELD
|
|
167
|
+
super().__init__(parent_model, admin_site)
|
|
168
|
+
|
|
169
|
+
def get_queryset(self, request):
|
|
170
|
+
return _prefetch_for(self.model, super().get_queryset(request))
|
|
171
|
+
|
|
172
|
+
def _get_avatar_obj(self, obj):
|
|
173
|
+
if hasattr(obj, "get_obj"):
|
|
174
|
+
return obj.get_obj()
|
|
175
|
+
return obj
|