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.
Files changed (29) hide show
  1. nilva_django_admin-0.2.0/LICENSE +21 -0
  2. nilva_django_admin-0.2.0/PKG-INFO +175 -0
  3. nilva_django_admin-0.2.0/README.md +150 -0
  4. nilva_django_admin-0.2.0/nilva_django_admin/__init__.py +15 -0
  5. nilva_django_admin-0.2.0/nilva_django_admin/admin_apps.py +19 -0
  6. nilva_django_admin-0.2.0/nilva_django_admin/apps.py +7 -0
  7. nilva_django_admin-0.2.0/nilva_django_admin/base.py +175 -0
  8. nilva_django_admin-0.2.0/nilva_django_admin/captcha.py +159 -0
  9. nilva_django_admin-0.2.0/nilva_django_admin/compat.py +47 -0
  10. nilva_django_admin-0.2.0/nilva_django_admin/settings.py +135 -0
  11. nilva_django_admin-0.2.0/nilva_django_admin/sites.py +95 -0
  12. nilva_django_admin-0.2.0/nilva_django_admin/sso.py +186 -0
  13. nilva_django_admin-0.2.0/nilva_django_admin/templates/nilva_django_admin/login.html +67 -0
  14. nilva_django_admin-0.2.0/nilva_django_admin/urls.py +15 -0
  15. nilva_django_admin-0.2.0/nilva_django_admin/views.py +76 -0
  16. nilva_django_admin-0.2.0/nilva_django_admin.egg-info/PKG-INFO +175 -0
  17. nilva_django_admin-0.2.0/nilva_django_admin.egg-info/SOURCES.txt +27 -0
  18. nilva_django_admin-0.2.0/nilva_django_admin.egg-info/dependency_links.txt +1 -0
  19. nilva_django_admin-0.2.0/nilva_django_admin.egg-info/requires.txt +8 -0
  20. nilva_django_admin-0.2.0/nilva_django_admin.egg-info/top_level.txt +1 -0
  21. nilva_django_admin-0.2.0/pyproject.toml +68 -0
  22. nilva_django_admin-0.2.0/setup.cfg +4 -0
  23. nilva_django_admin-0.2.0/tests/test_base.py +71 -0
  24. nilva_django_admin-0.2.0/tests/test_captcha.py +135 -0
  25. nilva_django_admin-0.2.0/tests/test_compat.py +25 -0
  26. nilva_django_admin-0.2.0/tests/test_package.py +13 -0
  27. nilva_django_admin-0.2.0/tests/test_settings.py +42 -0
  28. nilva_django_admin-0.2.0/tests/test_sites.py +94 -0
  29. 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,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class NilvaDjangoAdminConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "nilva_django_admin"
7
+ verbose_name = "Nilva Admin"
@@ -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