django-newadmin 0.1.0a1__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.
Files changed (45) hide show
  1. django_newadmin-0.1.0a1.dist-info/METADATA +139 -0
  2. django_newadmin-0.1.0a1.dist-info/RECORD +45 -0
  3. django_newadmin-0.1.0a1.dist-info/WHEEL +4 -0
  4. django_newadmin-0.1.0a1.dist-info/licenses/LICENSE +29 -0
  5. newadmin/__init__.py +3 -0
  6. newadmin/apps.py +13 -0
  7. newadmin/checks.py +28 -0
  8. newadmin/conf.py +168 -0
  9. newadmin/dashboard.py +72 -0
  10. newadmin/navigation.py +37 -0
  11. newadmin/static/newadmin/css/bootstrap.min.css +1 -0
  12. newadmin/static/newadmin/css/newadmin.css +2492 -0
  13. newadmin/static/newadmin/icons.svg +44 -0
  14. newadmin/static/newadmin/js/accessibility.js +42 -0
  15. newadmin/static/newadmin/js/appearance.js +51 -0
  16. newadmin/static/newadmin/js/dashboard-charts.js +95 -0
  17. newadmin/static/newadmin/js/navigation.js +38 -0
  18. newadmin/static/newadmin/vendor/bootstrap/LICENSE +21 -0
  19. newadmin/static/newadmin/vendor/bootstrap-icons/LICENSE +21 -0
  20. newadmin/static/newadmin/vendor/django/LICENSE +27 -0
  21. newadmin/static/newadmin/vendor/echarts/LICENSE +222 -0
  22. newadmin/static/newadmin/vendor/echarts/NOTICE +5 -0
  23. newadmin/static/newadmin/vendor/echarts/echarts.min.js +45 -0
  24. newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-d3 +27 -0
  25. newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-tslib +12 -0
  26. newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-zrender +29 -0
  27. newadmin/static/newadmin/vendor/echarts/licenses/NOTICE-tslib +15 -0
  28. newadmin/templates/admin/app_list.html +52 -0
  29. newadmin/templates/admin/auth/user/change_password.html +3 -0
  30. newadmin/templates/admin/base.html +19 -0
  31. newadmin/templates/admin/base_site.html +43 -0
  32. newadmin/templates/admin/change_form.html +3 -0
  33. newadmin/templates/admin/change_list.html +15 -0
  34. newadmin/templates/admin/filter.html +12 -0
  35. newadmin/templates/admin/index.html +108 -0
  36. newadmin/templates/admin/nav_sidebar.html +20 -0
  37. newadmin/templates/admin/object_history.html +3 -0
  38. newadmin/templates/newadmin/dashboard/chart.html +29 -0
  39. newadmin/templates/newadmin/dashboard/editorial.html +48 -0
  40. newadmin/templates/newadmin/dashboard/layout.html +23 -0
  41. newadmin/templates/newadmin/dashboard/metric.html +9 -0
  42. newadmin/templates/registration/password_change_done.html +3 -0
  43. newadmin/templates/registration/password_change_form.html +3 -0
  44. newadmin/templatetags/__init__.py +1 -0
  45. newadmin/templatetags/newadmin.py +74 -0
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-newadmin
3
+ Version: 0.1.0a1
4
+ Summary: A polished, extensible theme for Django's built-in admin.
5
+ Project-URL: Documentation, https://carocha.github.io/django-newadmin/
6
+ Project-URL: Repository, https://github.com/CARocha/django-newadmin
7
+ Project-URL: Issues, https://github.com/CARocha/django-newadmin/issues
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 6.0
13
+ Classifier: Framework :: Django :: 6.1
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
19
+ Classifier: Topic :: Software Development :: User Interfaces
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: django<6.2,>=6.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.2; extra == 'dev'
24
+ Requires-Dist: playwright>=1.52; extra == 'dev'
25
+ Requires-Dist: pytest-django>=4.11; extra == 'dev'
26
+ Requires-Dist: pytest>=8.3; extra == 'dev'
27
+ Requires-Dist: ruff>=0.11; extra == 'dev'
28
+ Requires-Dist: twine<8,>=7; extra == 'dev'
29
+ Provides-Extra: docs
30
+ Requires-Dist: furo<2027,>=2025.7.19; extra == 'docs'
31
+ Requires-Dist: myst-parser<6,>=4; extra == 'docs'
32
+ Requires-Dist: sphinx<10,>=8.2; extra == 'docs'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # django-newadmin
36
+
37
+ A modern, extensible light-and-dark theme for Django's built-in admin.
38
+
39
+ **Status:** 0.1.0a1 alpha candidate. Django 6.0 and 6.1 on Python 3.13 and 3.14
40
+ are tested locally; GitHub Actions repeats this matrix after the code is pushed.
41
+ TestPyPI publication is manual; PyPI publication runs only when a GitHub Release
42
+ uses the matching version tag. License: BSD-3-Clause.
43
+
44
+ **Documentation:** [CARocha.github.io/django-newadmin](https://carocha.github.io/django-newadmin/)
45
+ will be published by GitHub Actions after Pages is enabled for this repository.
46
+
47
+ ## Development setup
48
+
49
+ ```sh
50
+ python3.14 -m venv .venv
51
+ .venv/bin/python -m pip install -e '.[dev,docs]'
52
+ npm ci
53
+ .venv/bin/python -m pytest tests --ignore=tests/browser -q
54
+ .venv/bin/python -m playwright install chromium
55
+ .venv/bin/python -m pytest tests/browser -q
56
+ npm run build:css
57
+ npm run build:charts
58
+ npm run build:icons
59
+ npm run test:js
60
+ ```
61
+
62
+ The npm commands are for developing NewAdmin's assets. Installing the Python
63
+ package includes the compiled files and does not require Node.js.
64
+
65
+ ## Activate NewAdmin
66
+
67
+ Add `newadmin` before Django's admin app. No model changes, middleware, or
68
+ migrations are required in a consuming project:
69
+
70
+ ```python
71
+ INSTALLED_APPS = [
72
+ "newadmin",
73
+ "django.contrib.admin",
74
+ # Other Django apps.
75
+ ]
76
+ ```
77
+
78
+ The theme works with its defaults. Optional site branding and appearance can be
79
+ set in `settings.py`:
80
+
81
+ ```python
82
+ NEWADMIN = {
83
+ "SITE_TITLE": "Studio",
84
+ "SITE_HEADER": "Studio control room",
85
+ "DEFAULT_MODE": "auto", # light, dark, or auto
86
+ }
87
+ ```
88
+
89
+ ## Run the demo
90
+
91
+ After installing the project in editable mode, prepare the sample catalog and
92
+ create a local administrator:
93
+
94
+ ```sh
95
+ .venv/bin/python demo/manage.py migrate
96
+ .venv/bin/python demo/manage.py seed_demo
97
+ .venv/bin/python demo/manage.py createsuperuser
98
+ .venv/bin/python demo/manage.py runserver
99
+ ```
100
+
101
+ Open <http://127.0.0.1:8000/admin/> and sign in with the account you created.
102
+ The demo is a local development project and uses SQLite.
103
+
104
+ See the [project specification](SPEC.md), [design](DESIGN.md), and
105
+ [task list](tasks/todo.md) for scope and progress.
106
+
107
+ ## Custom dashboards
108
+
109
+ Use a Python provider and a template to show data from your own models. Optional
110
+ Apache ECharts graphs are bundled locally under Apache 2.0, with light/dark mode,
111
+ responsive SVG output and an HTML data alternative. No chart license purchase or
112
+ Node installation is required by a package consumer.
113
+
114
+ See the [dashboard guide](docs/dashboard.md) for configuration, permissions,
115
+ per-site providers, templates and examples. The demo enables an editorial provider
116
+ with entry counts, category charts and recent articles.
117
+
118
+ ## Menus and extensions
119
+
120
+ Configure model/app icons, named links with permission checks, per-AdminSite
121
+ settings and custom CSS. Public template blocks let a consuming project extend
122
+ the branding, sidebar and footer while retaining Django's native controls.
123
+ See the [configuration and extension guide](docs/configuration.md).
124
+
125
+ ## Documentation (Sphinx)
126
+
127
+ The Spanish user guides cover [installation](docs/installation.md),
128
+ [configuration](docs/configuration.md), [dashboards](docs/dashboard.md),
129
+ [accessibility](docs/accessibility.md) and [release steps](docs/releasing.md).
130
+ Build the searchable HTML documentation locally:
131
+
132
+ ```sh
133
+ .venv/bin/python -m sphinx -n -W --keep-going -b html docs docs/_build/html
134
+ .venv/bin/python -m http.server 8002 --directory docs/_build/html
135
+ ```
136
+
137
+ Open <http://127.0.0.1:8002/>. Read the [changelog](CHANGELOG.md) and alpha
138
+ limitations before using this release. `.readthedocs.yaml` is ready for connecting
139
+ a Read the Docs project; hosted documentation has not been published yet.
@@ -0,0 +1,45 @@
1
+ newadmin/__init__.py,sha256=FvmAOcUurKn1Z0X-SlNGQWJ0h0fWi84PWIxRFmOx8yg,85
2
+ newadmin/apps.py,sha256=0evky5EuK_2OG4y4luj89dmQp7d1-c5-c0Iuqmy2b4U,320
3
+ newadmin/checks.py,sha256=8TG2k3Q2YuI1j0ccYH8Vo5FPCZGPskPWT_E-NiZeZTc,1031
4
+ newadmin/conf.py,sha256=dTyR5opDrFzDOoykzPXfQK5FfgnfNqO76bkgefU3R0Q,6683
5
+ newadmin/dashboard.py,sha256=nxb578bQx4O9ddT0L-xJOQKZO8OIxWTsUgLBwi_GNaY,2646
6
+ newadmin/navigation.py,sha256=ujUYpNaberxwFdtOt6hwIcVWMt-sVAjE0-SW-ene08c,1297
7
+ newadmin/static/newadmin/icons.svg,sha256=4EaFfOnAGMrghpDb5OxVcHKHjF4R5ONPGrcj8-TU2g8,7895
8
+ newadmin/static/newadmin/css/bootstrap.min.css,sha256=rFNjE8xYWt8O5lgisjZyrCR2HEbk7hVfCvg6vndd5JU,111217
9
+ newadmin/static/newadmin/css/newadmin.css,sha256=YcvP3krRLLH2y6UIsurotUtJpOaTaB367ou4EszIuWM,68064
10
+ newadmin/static/newadmin/js/accessibility.js,sha256=kOfM8SRh9X9xJlK6eTK2pT-lkyl9YtoBKzvJblmIlQE,2240
11
+ newadmin/static/newadmin/js/appearance.js,sha256=4QKQnBYgH_oVjKO7bXJE0FYFLRE7o-LDP80OXAVuFDE,1614
12
+ newadmin/static/newadmin/js/dashboard-charts.js,sha256=cscShtxQk1beAvD_GRt0z_3Un5W9wCZMPd44nZkrfPA,3827
13
+ newadmin/static/newadmin/js/navigation.js,sha256=x92IO0oOllgW7OeU1xDwpA4-Iz6BeWFvAP4kjO11zxQ,1434
14
+ newadmin/static/newadmin/vendor/bootstrap/LICENSE,sha256=RiDIStXOhgL_ZWQO1rfIt467ngNlhPDrwczIggaku1E,1093
15
+ newadmin/static/newadmin/vendor/bootstrap-icons/LICENSE,sha256=D7PhG9V-iWxaUSr9ZIZNKKN95F0Zg1AWyHyhrRnq2Wk,1093
16
+ newadmin/static/newadmin/vendor/django/LICENSE,sha256=uEZBXRtRTpwd_xSiLeuQbXlLxUbKYSn5UKGM0JHipmk,1552
17
+ newadmin/static/newadmin/vendor/echarts/LICENSE,sha256=Y0KTg1tDpt0glPo5GCo9mmucpDt_25rDVOgDevKjCTo,11990
18
+ newadmin/static/newadmin/vendor/echarts/NOTICE,sha256=1JHTWDRPhCaFwbFYWXCZnbZf4w7PfvOGeviBT0AWwBY,168
19
+ newadmin/static/newadmin/vendor/echarts/echarts.min.js,sha256=tmslrrTfhOMxmdwhaUAU0zbSIsvZ3rDlp8FL1qoND9A,1121883
20
+ newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-d3,sha256=4SEYktoLDgWFt66-j5jBJ0-6Fbr-R_ofTuinpQLAYwQ,1475
21
+ newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-tslib,sha256=IQsZ5UMTA4jGhlS3SX6WcRnOFxRfZqt9hWiPvXDwh1E,655
22
+ newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-zrender,sha256=poHp0YPHK0r0hWk1iTgF60jAjw9aPyYCA9t0RMIPuYo,1509
23
+ newadmin/static/newadmin/vendor/echarts/licenses/NOTICE-tslib,sha256=3YNeZ6mil9oy-UZ9dnel0CyOUOEPFdpDuBAUfHnAS5s,824
24
+ newadmin/templates/admin/app_list.html,sha256=J2cYa3f1wXBGGQL6NUV0ay40WKE5WMoIIWZwc2t6J6g,2574
25
+ newadmin/templates/admin/base.html,sha256=P-9eYps8lEUfunt9ZOgZZctyVlJd1zYnk45dJWfN2KI,783
26
+ newadmin/templates/admin/base_site.html,sha256=YZMGGjgIadEjdJcFGLbOYYaye3MQ8DnOCUyoO9UlElY,1575
27
+ newadmin/templates/admin/change_form.html,sha256=0M0jb1S3dST6BH5Z9e-VyyZ_0XqPARducTDxX97xi3o,114
28
+ newadmin/templates/admin/change_list.html,sha256=q3LmE0KjobsX0Q7umZvAFEQEj_A9Js2Xj8fKrEcs_5c,547
29
+ newadmin/templates/admin/filter.html,sha256=7eu-BQTi7IAx4b1ucJevliMCmLaGlbnrfYKrgJqi0Tc,419
30
+ newadmin/templates/admin/index.html,sha256=XoZMLRLkzWhLDZHsMVn54o5xmlbcPDZ-Ivof5EfEdwQ,4712
31
+ newadmin/templates/admin/nav_sidebar.html,sha256=wMA-YeMeugtq1rEEHli6TbOpYUM4a61OCPkAnvIS1O0,1403
32
+ newadmin/templates/admin/object_history.html,sha256=kVCnXNr5ll7yx11IzNKktZG23pYeNKlEU59N6jxYbU0,120
33
+ newadmin/templates/admin/auth/user/change_password.html,sha256=Gas4D1cbc8NCsNRN8fAak5lZ6mFkY7_LVJtOEL_aL1E,128
34
+ newadmin/templates/newadmin/dashboard/chart.html,sha256=eSGM66YCzNlCNcmF4ecJefqzTMTGPz9IjJ-oyzpeQQo,1430
35
+ newadmin/templates/newadmin/dashboard/editorial.html,sha256=ktz-9uZ2FMm3xUSbahGdVoJFohwKJcCj5lDT0oF7bP8,2525
36
+ newadmin/templates/newadmin/dashboard/layout.html,sha256=ErWJE7MgDm5UufFU1-xTOCppCsyZa02kcUGmAte9q2Q,978
37
+ newadmin/templates/newadmin/dashboard/metric.html,sha256=UudPFXpxSX4XnVbzecKW3DcbIE66AiEm3jj8Wzvi0jU,417
38
+ newadmin/templates/registration/password_change_done.html,sha256=sKqLpoJ6Q1sLgMissaq3ry_hVcIdXyXdqlKWakWzlmk,139
39
+ newadmin/templates/registration/password_change_form.html,sha256=kwPbtL2S508kCCOwa-sahXqH_sFDGG9GOKF7WlG3rbU,130
40
+ newadmin/templatetags/__init__.py,sha256=w-XVzoWOfsQvMW4MJikPlJgXbxZQGrNaygE0uNcEFpU,30
41
+ newadmin/templatetags/newadmin.py,sha256=UcxtYTaemko27X_fNOM29YGq_0jV5GQvITLTJVijt0o,2487
42
+ django_newadmin-0.1.0a1.dist-info/METADATA,sha256=IfbmgKM_t6vu3ChBv-75xYwrEnJSaNHIemJ9V8BBOiQ,5089
43
+ django_newadmin-0.1.0a1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
44
+ django_newadmin-0.1.0a1.dist-info/licenses/LICENSE,sha256=s3-nmFNTc-wUV0h3j87sq0EOzgHGTHMdIFnX8IqtzjA,1532
45
+ django_newadmin-0.1.0a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, CARocha and contributors
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software without
18
+ specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
newadmin/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Modern, extensible theme for Django's built-in admin."""
2
+
3
+ __version__ = "0.1.0a1"
newadmin/apps.py ADDED
@@ -0,0 +1,13 @@
1
+ """Application definition for the NewAdmin theme."""
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class NewAdminConfig(AppConfig):
7
+ """Register NewAdmin as a lightweight Django application."""
8
+
9
+ name = "newadmin"
10
+ verbose_name = "Django NewAdmin"
11
+
12
+ def ready(self):
13
+ from newadmin import checks # noqa: F401
newadmin/checks.py ADDED
@@ -0,0 +1,28 @@
1
+ """Django system checks for common NewAdmin configuration errors."""
2
+
3
+ from django.conf import settings
4
+ from django.core.checks import Error, register
5
+ from django.core.exceptions import ImproperlyConfigured
6
+
7
+ from newadmin.conf import get_options
8
+
9
+
10
+ @register("admin")
11
+ def check_newadmin(app_configs, **kwargs):
12
+ errors = []
13
+ installed_apps = getattr(settings, "INSTALLED_APPS", [])
14
+ if "newadmin" in installed_apps and "django.contrib.admin" in installed_apps:
15
+ if installed_apps.index("newadmin") > installed_apps.index("django.contrib.admin"):
16
+ errors.append(
17
+ Error(
18
+ "Add 'newadmin' before 'django.contrib.admin' in INSTALLED_APPS.",
19
+ id="newadmin.E005",
20
+ )
21
+ )
22
+ try:
23
+ options = get_options()
24
+ for site_name in options["SITES"]:
25
+ get_options(site_name)
26
+ except ImproperlyConfigured as exc:
27
+ errors.append(Error(str(exc), id=getattr(exc, "check_id", "newadmin.E002")))
28
+ return errors
newadmin/conf.py ADDED
@@ -0,0 +1,168 @@
1
+ """Validated settings for NewAdmin."""
2
+
3
+ from collections.abc import Mapping
4
+ from copy import deepcopy
5
+
6
+ from django.conf import settings
7
+ from django.core.exceptions import ImproperlyConfigured
8
+ from django.utils.functional import Promise
9
+
10
+ DEFAULTS = {
11
+ "SITE_TITLE": None,
12
+ "SITE_HEADER": None,
13
+ "LOGO": None,
14
+ "DEFAULT_MODE": "auto",
15
+ "PALETTE": "sapphire",
16
+ "MENU": {"ORDER": [], "ICONS": {}, "LINKS": []},
17
+ "CUSTOM_CSS": [],
18
+ "DASHBOARD": None,
19
+ "SITES": {},
20
+ }
21
+ VALID_MODES = {"light", "dark", "auto"}
22
+ ICON_NAMES = {
23
+ "grid",
24
+ "file-text",
25
+ "users",
26
+ "settings",
27
+ "folder",
28
+ "image",
29
+ "calendar",
30
+ "chart",
31
+ "tag",
32
+ "database",
33
+ "globe",
34
+ "link",
35
+ }
36
+
37
+
38
+ class _SettingsError(ImproperlyConfigured):
39
+ check_id = "newadmin.E001"
40
+
41
+
42
+ class _ResourceError(ImproperlyConfigured):
43
+ check_id = "newadmin.E003"
44
+
45
+
46
+ class _MenuError(ImproperlyConfigured):
47
+ check_id = "newadmin.E004"
48
+
49
+
50
+ def _merge(base, overrides):
51
+ result = deepcopy(base)
52
+ for key, value in overrides.items():
53
+ if key in result and isinstance(result[key], dict) and isinstance(value, Mapping):
54
+ result[key] = _merge(result[key], value)
55
+ else:
56
+ result[key] = deepcopy(value)
57
+ return result
58
+
59
+
60
+ def _validate_resource_path(value, option):
61
+ if value is None and option == "LOGO":
62
+ return
63
+ paths = [value] if option == "LOGO" else value
64
+ if option == "CUSTOM_CSS" and not isinstance(paths, list):
65
+ raise _ResourceError("NEWADMIN.CUSTOM_CSS must be a list of static paths.")
66
+ for path in paths:
67
+ if (
68
+ not isinstance(path, str)
69
+ or not path
70
+ or path.startswith(("/", "\\"))
71
+ or ".." in path.split("/")
72
+ or "\\" in path
73
+ or any(char in path for char in (":", "?", "#"))
74
+ ):
75
+ raise _ResourceError(f"NEWADMIN.{option} must contain safe relative static paths.")
76
+
77
+
78
+ def get_options(site_name=None):
79
+ """Return a fresh, validated settings mapping for an optional AdminSite."""
80
+ raw = getattr(settings, "NEWADMIN", {})
81
+ if not isinstance(raw, Mapping):
82
+ raise _SettingsError("NEWADMIN must be a mapping of supported options.")
83
+ unknown = set(raw) - set(DEFAULTS)
84
+ if unknown:
85
+ raise ImproperlyConfigured(
86
+ f"Unknown NEWADMIN option(s): {', '.join(sorted(map(str, unknown)))}."
87
+ )
88
+
89
+ options = _merge(DEFAULTS, raw)
90
+ if not isinstance(options["SITES"], Mapping):
91
+ raise _SettingsError("NEWADMIN.SITES must be a mapping of AdminSite names to options.")
92
+ if site_name is not None and site_name in options["SITES"]:
93
+ site_options = options["SITES"][site_name]
94
+ if not isinstance(site_options, Mapping):
95
+ raise _SettingsError(f"NEWADMIN.SITES.{site_name} must be a mapping.")
96
+ unknown_site = set(site_options) - (set(DEFAULTS) - {"SITES"})
97
+ if unknown_site:
98
+ raise ImproperlyConfigured(
99
+ f"Unknown NEWADMIN.SITES.{site_name} option(s): "
100
+ f"{', '.join(sorted(map(str, unknown_site)))}."
101
+ )
102
+ options = _merge(options, site_options)
103
+
104
+ if not isinstance(options["DEFAULT_MODE"], str) or options["DEFAULT_MODE"] not in VALID_MODES:
105
+ raise ImproperlyConfigured("NEWADMIN.DEFAULT_MODE must be 'light', 'dark', or 'auto'.")
106
+ if options["PALETTE"] != "sapphire":
107
+ raise ImproperlyConfigured("NEWADMIN.PALETTE must be 'sapphire'.")
108
+ _validate_resource_path(options["LOGO"], "LOGO")
109
+ _validate_resource_path(options["CUSTOM_CSS"], "CUSTOM_CSS")
110
+ if options["DASHBOARD"] is not None and (
111
+ not isinstance(options["DASHBOARD"], str) or not options["DASHBOARD"].strip()
112
+ ):
113
+ raise ImproperlyConfigured("NEWADMIN.DASHBOARD must be an importable class path or None.")
114
+ if not isinstance(options["MENU"], Mapping):
115
+ raise _MenuError("NEWADMIN.MENU must be a mapping.")
116
+ menu_keys = {"ORDER", "ICONS", "LINKS"}
117
+ unknown_menu_keys = set(options["MENU"]) - menu_keys
118
+ if unknown_menu_keys:
119
+ raise _MenuError(
120
+ f"Unknown NEWADMIN.MENU option(s): {', '.join(sorted(map(str, unknown_menu_keys)))}."
121
+ )
122
+ order = options["MENU"].get("ORDER", [])
123
+ if not isinstance(order, list) or any(not isinstance(label, str) for label in order):
124
+ raise _MenuError("NEWADMIN.MENU.ORDER must be a list of app labels.")
125
+ if len(order) != len(set(order)):
126
+ raise _MenuError("NEWADMIN.MENU.ORDER cannot contain duplicate app labels.")
127
+ icons = options["MENU"].get("ICONS", {})
128
+ if not isinstance(icons, Mapping):
129
+ raise _MenuError("NEWADMIN.MENU.ICONS must map app/model names to icon names.")
130
+ for key, icon in icons.items():
131
+ if (
132
+ not isinstance(key, str)
133
+ or not key
134
+ or not isinstance(icon, str)
135
+ or icon not in ICON_NAMES
136
+ ):
137
+ raise _MenuError(f"NEWADMIN.MENU.ICONS.{key} must use a supported icon name.")
138
+ links = options["MENU"].get("LINKS", [])
139
+ if not isinstance(links, list):
140
+ raise _MenuError("NEWADMIN.MENU.LINKS must be a list of link mappings.")
141
+ for index, link in enumerate(links):
142
+ path = f"NEWADMIN.MENU.LINKS[{index}]"
143
+ if not isinstance(link, Mapping):
144
+ raise _MenuError(f"{path} must be a mapping.")
145
+ unknown_link = set(link) - {"LABEL", "URL_NAME", "ICON", "PERMISSIONS"}
146
+ if unknown_link:
147
+ raise _MenuError(
148
+ f"{path} has unknown option(s): {', '.join(sorted(map(str, unknown_link)))}."
149
+ )
150
+ for key in ("LABEL", "URL_NAME"):
151
+ types = (str, Promise) if key == "LABEL" else str
152
+ if not isinstance(link.get(key), types) or not str(link[key]).strip():
153
+ raise _MenuError(f"{path}.{key} must be non-empty text.")
154
+ if "ICON" in link and (not isinstance(link["ICON"], str) or link["ICON"] not in ICON_NAMES):
155
+ raise _MenuError(f"{path}.ICON must name a supported icon.")
156
+ permissions = link.get("PERMISSIONS", [])
157
+ if not isinstance(permissions, list) or any(
158
+ not isinstance(permission, str)
159
+ or permission.count(".") != 1
160
+ or not all(permission.split("."))
161
+ or any(char.isspace() for char in permission)
162
+ for permission in permissions
163
+ ):
164
+ raise _MenuError(f"{path}.PERMISSIONS must be a list of app.codename values.")
165
+ for name in ("SITE_TITLE", "SITE_HEADER"):
166
+ if options[name] is not None and not isinstance(options[name], str):
167
+ raise ImproperlyConfigured(f"NEWADMIN.{name} must be text or None.")
168
+ return options
newadmin/dashboard.py ADDED
@@ -0,0 +1,72 @@
1
+ """Request-scoped extension point for the admin's main dashboard."""
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+
6
+ from django.core.exceptions import ImproperlyConfigured
7
+ from django.forms import Media
8
+ from django.forms.widgets import MediaDefiningClass
9
+ from django.utils.module_loading import import_string
10
+
11
+ from newadmin.conf import get_options
12
+
13
+
14
+ class Dashboard(metaclass=MediaDefiningClass):
15
+ """Provide a template, request data and optional Media for the admin index."""
16
+
17
+ template_name = "newadmin/dashboard/layout.html"
18
+ permissions = ()
19
+
20
+ def has_permission(self, request):
21
+ return (
22
+ request.user.is_active
23
+ and request.user.is_staff
24
+ and request.user.has_perms(self.permissions)
25
+ )
26
+
27
+ def get_context_data(self, request):
28
+ return {}
29
+
30
+
31
+ class EChartsDashboard(Dashboard):
32
+ """Opt in to the packaged, local Apache ECharts runtime and adapter."""
33
+
34
+ class Media:
35
+ js = ["newadmin/vendor/echarts/echarts.min.js", "newadmin/js/dashboard-charts.js"]
36
+
37
+
38
+ @dataclass
39
+ class _ResolvedDashboard:
40
+ template_name: str
41
+ data: Mapping
42
+ media: Media
43
+
44
+
45
+ def resolve_dashboard(request):
46
+ """Resolve once per request; permission checks precede data and Media evaluation."""
47
+ if hasattr(request, "_newadmin_dashboard"):
48
+ return request._newadmin_dashboard
49
+ site_name = getattr(request, "current_app", None) or getattr(
50
+ getattr(request, "resolver_match", None), "namespace", None
51
+ )
52
+ path = get_options(site_name)["DASHBOARD"]
53
+ result = None
54
+ if path is not None:
55
+ try:
56
+ provider_class = import_string(path)
57
+ except (ImportError, AttributeError) as exc:
58
+ raise ImproperlyConfigured(f"NEWADMIN.DASHBOARD cannot import {path!r}.") from exc
59
+ if not isinstance(provider_class, type) or not issubclass(provider_class, Dashboard):
60
+ raise ImproperlyConfigured("NEWADMIN.DASHBOARD must name a Dashboard subclass.")
61
+ provider = provider_class()
62
+ if provider.has_permission(request):
63
+ data = provider.get_context_data(request)
64
+ if not isinstance(data, Mapping):
65
+ raise ImproperlyConfigured("NEWADMIN.DASHBOARD context must be a mapping.")
66
+ if not isinstance(provider.template_name, str) or not provider.template_name:
67
+ raise ImproperlyConfigured(
68
+ "NEWADMIN.DASHBOARD template_name must be non-empty text."
69
+ )
70
+ result = _ResolvedDashboard(provider.template_name, data, provider.media)
71
+ request._newadmin_dashboard = result
72
+ return result
newadmin/navigation.py ADDED
@@ -0,0 +1,37 @@
1
+ """Request-specific navigation derived from the active admin site."""
2
+
3
+ from django.core.exceptions import ImproperlyConfigured
4
+ from django.urls import NoReverseMatch, reverse
5
+ from django.utils.encoding import iri_to_uri
6
+
7
+ from newadmin.conf import get_options
8
+
9
+
10
+ def site_name(request):
11
+ match = getattr(request, "resolver_match", None)
12
+ return getattr(request, "current_app", None) or getattr(match, "namespace", None)
13
+
14
+
15
+ def custom_links(request):
16
+ name = site_name(request)
17
+ options = get_options(name)
18
+ user = getattr(request, "user", None)
19
+ links = []
20
+ for index, item in enumerate(options["MENU"]["LINKS"]):
21
+ if item.get("PERMISSIONS") and (user is None or not user.has_perms(item["PERMISSIONS"])):
22
+ continue
23
+ try:
24
+ url = reverse(item["URL_NAME"], current_app=name)
25
+ except NoReverseMatch as exc:
26
+ raise ImproperlyConfigured(
27
+ f"NEWADMIN.MENU.LINKS[{index}].URL_NAME cannot be resolved: {item['URL_NAME']!r}."
28
+ ) from exc
29
+ links.append(
30
+ {
31
+ "label": item["LABEL"],
32
+ "url": url,
33
+ "icon": item.get("ICON", "link"),
34
+ "current": iri_to_uri(getattr(request, "path", "")) == url,
35
+ }
36
+ )
37
+ return links