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.
- django_newadmin-0.1.0a1.dist-info/METADATA +139 -0
- django_newadmin-0.1.0a1.dist-info/RECORD +45 -0
- django_newadmin-0.1.0a1.dist-info/WHEEL +4 -0
- django_newadmin-0.1.0a1.dist-info/licenses/LICENSE +29 -0
- newadmin/__init__.py +3 -0
- newadmin/apps.py +13 -0
- newadmin/checks.py +28 -0
- newadmin/conf.py +168 -0
- newadmin/dashboard.py +72 -0
- newadmin/navigation.py +37 -0
- newadmin/static/newadmin/css/bootstrap.min.css +1 -0
- newadmin/static/newadmin/css/newadmin.css +2492 -0
- newadmin/static/newadmin/icons.svg +44 -0
- newadmin/static/newadmin/js/accessibility.js +42 -0
- newadmin/static/newadmin/js/appearance.js +51 -0
- newadmin/static/newadmin/js/dashboard-charts.js +95 -0
- newadmin/static/newadmin/js/navigation.js +38 -0
- newadmin/static/newadmin/vendor/bootstrap/LICENSE +21 -0
- newadmin/static/newadmin/vendor/bootstrap-icons/LICENSE +21 -0
- newadmin/static/newadmin/vendor/django/LICENSE +27 -0
- newadmin/static/newadmin/vendor/echarts/LICENSE +222 -0
- newadmin/static/newadmin/vendor/echarts/NOTICE +5 -0
- newadmin/static/newadmin/vendor/echarts/echarts.min.js +45 -0
- newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-d3 +27 -0
- newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-tslib +12 -0
- newadmin/static/newadmin/vendor/echarts/licenses/LICENSE-zrender +29 -0
- newadmin/static/newadmin/vendor/echarts/licenses/NOTICE-tslib +15 -0
- newadmin/templates/admin/app_list.html +52 -0
- newadmin/templates/admin/auth/user/change_password.html +3 -0
- newadmin/templates/admin/base.html +19 -0
- newadmin/templates/admin/base_site.html +43 -0
- newadmin/templates/admin/change_form.html +3 -0
- newadmin/templates/admin/change_list.html +15 -0
- newadmin/templates/admin/filter.html +12 -0
- newadmin/templates/admin/index.html +108 -0
- newadmin/templates/admin/nav_sidebar.html +20 -0
- newadmin/templates/admin/object_history.html +3 -0
- newadmin/templates/newadmin/dashboard/chart.html +29 -0
- newadmin/templates/newadmin/dashboard/editorial.html +48 -0
- newadmin/templates/newadmin/dashboard/layout.html +23 -0
- newadmin/templates/newadmin/dashboard/metric.html +9 -0
- newadmin/templates/registration/password_change_done.html +3 -0
- newadmin/templates/registration/password_change_form.html +3 -0
- newadmin/templatetags/__init__.py +1 -0
- 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,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
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
|