django-admin-home 0.1.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.
- django_admin_home-0.1.0/LICENSE +21 -0
- django_admin_home-0.1.0/PKG-INFO +142 -0
- django_admin_home-0.1.0/README.md +109 -0
- django_admin_home-0.1.0/django_admin_home/__init__.py +106 -0
- django_admin_home-0.1.0/django_admin_home/apps.py +10 -0
- django_admin_home-0.1.0/django_admin_home/install.py +145 -0
- django_admin_home-0.1.0/django_admin_home/menu.py +129 -0
- django_admin_home-0.1.0/django_admin_home/migrations/0001_initial.py +64 -0
- django_admin_home-0.1.0/django_admin_home/migrations/__init__.py +0 -0
- django_admin_home-0.1.0/django_admin_home/models.py +45 -0
- django_admin_home-0.1.0/django_admin_home/pages.py +108 -0
- django_admin_home-0.1.0/django_admin_home/static/django_admin_home/css/home.css +332 -0
- django_admin_home-0.1.0/django_admin_home/static/django_admin_home/css/nav.css +371 -0
- django_admin_home-0.1.0/django_admin_home/static/django_admin_home/js/nav.js +331 -0
- django_admin_home-0.1.0/django_admin_home/templates/admin/index.html +124 -0
- django_admin_home-0.1.0/django_admin_home/templates/admin/nav_sidebar.html +88 -0
- django_admin_home-0.1.0/django_admin_home/templates/admin_home/_brand.html +6 -0
- django_admin_home-0.1.0/django_admin_home/templates/admin_home/_icon_sprite.html +29 -0
- django_admin_home-0.1.0/django_admin_home/views.py +34 -0
- django_admin_home-0.1.0/django_admin_home.egg-info/PKG-INFO +142 -0
- django_admin_home-0.1.0/django_admin_home.egg-info/SOURCES.txt +29 -0
- django_admin_home-0.1.0/django_admin_home.egg-info/dependency_links.txt +1 -0
- django_admin_home-0.1.0/django_admin_home.egg-info/requires.txt +5 -0
- django_admin_home-0.1.0/django_admin_home.egg-info/top_level.txt +1 -0
- django_admin_home-0.1.0/pyproject.toml +80 -0
- django_admin_home-0.1.0/setup.cfg +4 -0
- django_admin_home-0.1.0/tests/test_install.py +82 -0
- django_admin_home-0.1.0/tests/test_menu.py +101 -0
- django_admin_home-0.1.0/tests/test_pages.py +64 -0
- django_admin_home-0.1.0/tests/test_public_api.py +11 -0
- django_admin_home-0.1.0/tests/test_views.py +54 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabio Valle
|
|
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,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-admin-home
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A tree-navigation sidebar plus a favorites/most-accessed home dashboard for the Django admin
|
|
5
|
+
Author: Fabio Valle
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fdelvalle/django-admin-home
|
|
8
|
+
Project-URL: Issues, https://github.com/fdelvalle/django-admin-home/issues
|
|
9
|
+
Keywords: django,admin,dashboard,sidebar,navigation,django-admin
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: Django :: 4.2
|
|
14
|
+
Classifier: Framework :: Django :: 5.0
|
|
15
|
+
Classifier: Framework :: Django :: 5.1
|
|
16
|
+
Classifier: Framework :: Django :: 5.2
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: Django>=4.2
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-django>=4.8; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# django-admin-home
|
|
35
|
+
|
|
36
|
+
A tree-navigation sidebar plus a favorites/most-accessed home dashboard for
|
|
37
|
+
the Django admin.
|
|
38
|
+
|
|
39
|
+
By default, the Django admin's home page is a flat, alphabetical list of
|
|
40
|
+
every app/model the current user can access, and the built-in sidebar has
|
|
41
|
+
no favorites or usage-based shortcuts. This package replaces both with:
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- A collapsible sidebar, grouped by app, built from the admin's own
|
|
46
|
+
`get_app_list` — so it always respects the current user's permissions.
|
|
47
|
+
- A "Favorites" section and a star to pin/unpin any app or model
|
|
48
|
+
(persisted per user).
|
|
49
|
+
- A home page with cards for favorites, most-accessed items (tracked per
|
|
50
|
+
user), and every module, in a responsive grid (with an optional compact
|
|
51
|
+
"masonry" layout).
|
|
52
|
+
- An optional "Pages" group for custom, non-model links (dashboards, API
|
|
53
|
+
docs, external tools, ...), entirely configured via settings.
|
|
54
|
+
- A dependency-free, offline SVG icon set (no external font/CDN).
|
|
55
|
+
- Defensive by design: any unexpected failure falls back to the native
|
|
56
|
+
admin behaviour instead of breaking the page.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install django-admin-home
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Add it to `INSTALLED_APPS` (it ships models, so run `migrate` afterwards):
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
INSTALLED_APPS = [
|
|
68
|
+
"django_admin_home",
|
|
69
|
+
...
|
|
70
|
+
"django.contrib.admin",
|
|
71
|
+
]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Enable it, once — e.g. in your own app's `AppConfig.ready()`:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from django.apps import AppConfig
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MyAppConfig(AppConfig):
|
|
81
|
+
def ready(self):
|
|
82
|
+
from django_admin_home import install
|
|
83
|
+
|
|
84
|
+
install()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Include the bundled CSS/JS in your `admin/base_site.html`:
|
|
88
|
+
|
|
89
|
+
```django
|
|
90
|
+
{% load static %}
|
|
91
|
+
<link rel="stylesheet" href="{% static 'django_admin_home/css/nav.css' %}">
|
|
92
|
+
<link rel="stylesheet" href="{% static 'django_admin_home/css/home.css' %}">
|
|
93
|
+
<script src="{% static 'django_admin_home/js/nav.js' %}" defer></script>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Run migrations:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python manage.py migrate django_admin_home
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Settings (all optional)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Icon per app/model. Value is a symbol name from the bundled SVG sprite
|
|
106
|
+
# (admin_home/_icon_sprite.html) — add your own <symbol> there via a
|
|
107
|
+
# template override if you need more icons.
|
|
108
|
+
ADMIN_HOME_APP_ICONS = {"buyers": "building", "cards": "card"}
|
|
109
|
+
ADMIN_HOME_MODEL_ICONS = {"buyers.buyer": "building"}
|
|
110
|
+
|
|
111
|
+
# Extra, non-model links shown in a "Pages" group.
|
|
112
|
+
ADMIN_HOME_CUSTOM_PAGES = [
|
|
113
|
+
{
|
|
114
|
+
"key": "page.dashboard",
|
|
115
|
+
"name": "Dashboard",
|
|
116
|
+
"icon": "gauge",
|
|
117
|
+
"url_name": "dashboard_index",
|
|
118
|
+
"permission": "account.view_menu_dashboard", # optional
|
|
119
|
+
"new_tab": True,
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# How many "most accessed" cards to show on the home page (default 8).
|
|
124
|
+
ADMIN_HOME_MAX_MOST_ACCESSED = 8
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Overriding the brand/logo
|
|
128
|
+
|
|
129
|
+
The sidebar header includes `admin_home/_brand.html`, which by default just
|
|
130
|
+
shows `site_header` as text. To show your own logo, place a template at
|
|
131
|
+
the same path earlier in your project's template resolution (e.g.
|
|
132
|
+
`templates/admin_home/_brand.html` in your project, with `APP_DIRS` search
|
|
133
|
+
order putting your project templates before installed apps).
|
|
134
|
+
|
|
135
|
+
## What this package intentionally does not do
|
|
136
|
+
|
|
137
|
+
- It does not set `site_header` / `site_title` / `index_title` — that
|
|
138
|
+
stays a project-level decision.
|
|
139
|
+
- It does not touch `AdminSite.has_permission` — any extra access rules
|
|
140
|
+
are the host project's responsibility.
|
|
141
|
+
- It does not migrate data from a previous, project-specific
|
|
142
|
+
favorites/access-tracking implementation.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# django-admin-home
|
|
2
|
+
|
|
3
|
+
A tree-navigation sidebar plus a favorites/most-accessed home dashboard for
|
|
4
|
+
the Django admin.
|
|
5
|
+
|
|
6
|
+
By default, the Django admin's home page is a flat, alphabetical list of
|
|
7
|
+
every app/model the current user can access, and the built-in sidebar has
|
|
8
|
+
no favorites or usage-based shortcuts. This package replaces both with:
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- A collapsible sidebar, grouped by app, built from the admin's own
|
|
13
|
+
`get_app_list` — so it always respects the current user's permissions.
|
|
14
|
+
- A "Favorites" section and a star to pin/unpin any app or model
|
|
15
|
+
(persisted per user).
|
|
16
|
+
- A home page with cards for favorites, most-accessed items (tracked per
|
|
17
|
+
user), and every module, in a responsive grid (with an optional compact
|
|
18
|
+
"masonry" layout).
|
|
19
|
+
- An optional "Pages" group for custom, non-model links (dashboards, API
|
|
20
|
+
docs, external tools, ...), entirely configured via settings.
|
|
21
|
+
- A dependency-free, offline SVG icon set (no external font/CDN).
|
|
22
|
+
- Defensive by design: any unexpected failure falls back to the native
|
|
23
|
+
admin behaviour instead of breaking the page.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install django-admin-home
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add it to `INSTALLED_APPS` (it ships models, so run `migrate` afterwards):
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
INSTALLED_APPS = [
|
|
35
|
+
"django_admin_home",
|
|
36
|
+
...
|
|
37
|
+
"django.contrib.admin",
|
|
38
|
+
]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Enable it, once — e.g. in your own app's `AppConfig.ready()`:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from django.apps import AppConfig
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MyAppConfig(AppConfig):
|
|
48
|
+
def ready(self):
|
|
49
|
+
from django_admin_home import install
|
|
50
|
+
|
|
51
|
+
install()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Include the bundled CSS/JS in your `admin/base_site.html`:
|
|
55
|
+
|
|
56
|
+
```django
|
|
57
|
+
{% load static %}
|
|
58
|
+
<link rel="stylesheet" href="{% static 'django_admin_home/css/nav.css' %}">
|
|
59
|
+
<link rel="stylesheet" href="{% static 'django_admin_home/css/home.css' %}">
|
|
60
|
+
<script src="{% static 'django_admin_home/js/nav.js' %}" defer></script>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Run migrations:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python manage.py migrate django_admin_home
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Settings (all optional)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# Icon per app/model. Value is a symbol name from the bundled SVG sprite
|
|
73
|
+
# (admin_home/_icon_sprite.html) — add your own <symbol> there via a
|
|
74
|
+
# template override if you need more icons.
|
|
75
|
+
ADMIN_HOME_APP_ICONS = {"buyers": "building", "cards": "card"}
|
|
76
|
+
ADMIN_HOME_MODEL_ICONS = {"buyers.buyer": "building"}
|
|
77
|
+
|
|
78
|
+
# Extra, non-model links shown in a "Pages" group.
|
|
79
|
+
ADMIN_HOME_CUSTOM_PAGES = [
|
|
80
|
+
{
|
|
81
|
+
"key": "page.dashboard",
|
|
82
|
+
"name": "Dashboard",
|
|
83
|
+
"icon": "gauge",
|
|
84
|
+
"url_name": "dashboard_index",
|
|
85
|
+
"permission": "account.view_menu_dashboard", # optional
|
|
86
|
+
"new_tab": True,
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# How many "most accessed" cards to show on the home page (default 8).
|
|
91
|
+
ADMIN_HOME_MAX_MOST_ACCESSED = 8
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Overriding the brand/logo
|
|
95
|
+
|
|
96
|
+
The sidebar header includes `admin_home/_brand.html`, which by default just
|
|
97
|
+
shows `site_header` as text. To show your own logo, place a template at
|
|
98
|
+
the same path earlier in your project's template resolution (e.g.
|
|
99
|
+
`templates/admin_home/_brand.html` in your project, with `APP_DIRS` search
|
|
100
|
+
order putting your project templates before installed apps).
|
|
101
|
+
|
|
102
|
+
## What this package intentionally does not do
|
|
103
|
+
|
|
104
|
+
- It does not set `site_header` / `site_title` / `index_title` — that
|
|
105
|
+
stays a project-level decision.
|
|
106
|
+
- It does not touch `AdminSite.has_permission` — any extra access rules
|
|
107
|
+
are the host project's responsibility.
|
|
108
|
+
- It does not migrate data from a previous, project-specific
|
|
109
|
+
favorites/access-tracking implementation.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""A tree-navigation sidebar + favorites/most-accessed home dashboard for the Django admin.
|
|
2
|
+
|
|
3
|
+
By default, the Django admin's home page is a flat, alphabetical list of
|
|
4
|
+
every app/model the user can access, and the built-in sidebar has no
|
|
5
|
+
favorites or usage-based shortcuts. This package replaces both with:
|
|
6
|
+
|
|
7
|
+
- A collapsible sidebar, grouped by app, with a "Favorites" section and a
|
|
8
|
+
star to pin/unpin any item (persisted per user).
|
|
9
|
+
- A home page with cards for favorites, most-accessed items (tracked per
|
|
10
|
+
user), and every module, in a responsive grid (with an optional compact
|
|
11
|
+
"masonry" layout).
|
|
12
|
+
- An optional "Pages" group for custom, non-model links (dashboards, API
|
|
13
|
+
docs, ...), configured entirely via settings.
|
|
14
|
+
- A dependency-free SVG icon set, offline (no external font/CDN).
|
|
15
|
+
|
|
16
|
+
Installation
|
|
17
|
+
------------
|
|
18
|
+
1. ``pip install django-admin-home``
|
|
19
|
+
|
|
20
|
+
2. Add ``"django_admin_home"`` to ``INSTALLED_APPS`` (it ships models, so
|
|
21
|
+
run ``migrate`` afterwards)::
|
|
22
|
+
|
|
23
|
+
INSTALLED_APPS = [
|
|
24
|
+
"django_admin_home",
|
|
25
|
+
...
|
|
26
|
+
"django.contrib.admin",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
3. Enable it, once — e.g. in your own app's ``AppConfig.ready()``::
|
|
30
|
+
|
|
31
|
+
from django_admin_home import install
|
|
32
|
+
|
|
33
|
+
class MyAppConfig(AppConfig):
|
|
34
|
+
def ready(self):
|
|
35
|
+
install()
|
|
36
|
+
|
|
37
|
+
4. Include the bundled CSS/JS in your ``admin/base_site.html``::
|
|
38
|
+
|
|
39
|
+
{% load static %}
|
|
40
|
+
<link rel="stylesheet" href="{% static 'django_admin_home/css/nav.css' %}">
|
|
41
|
+
<link rel="stylesheet" href="{% static 'django_admin_home/css/home.css' %}">
|
|
42
|
+
<script src="{% static 'django_admin_home/js/nav.js' %}" defer></script>
|
|
43
|
+
|
|
44
|
+
Optional settings
|
|
45
|
+
------------------
|
|
46
|
+
- ``ADMIN_HOME_APP_ICONS`` / ``ADMIN_HOME_MODEL_ICONS``: dicts mapping an
|
|
47
|
+
``app_label`` (or ``app_label.model_name``) to an icon symbol name.
|
|
48
|
+
- ``ADMIN_HOME_CUSTOM_PAGES``: list of dicts describing extra, non-model
|
|
49
|
+
links shown in a "Pages" group. See :mod:`django_admin_home.pages`.
|
|
50
|
+
- ``ADMIN_HOME_MAX_MOST_ACCESSED``: how many "most accessed" cards to show
|
|
51
|
+
on the home page (default 8).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from __future__ import annotations
|
|
55
|
+
|
|
56
|
+
__version__ = "0.1.0"
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"DEFAULT_APP_ICON",
|
|
60
|
+
"DEFAULT_MODEL_ICON",
|
|
61
|
+
"HOME_ICON",
|
|
62
|
+
"MenuAccess",
|
|
63
|
+
"MenuFavorite",
|
|
64
|
+
"app_menu_key",
|
|
65
|
+
"build_custom_pages_group",
|
|
66
|
+
"build_menu_tree",
|
|
67
|
+
"flatten_menu_items",
|
|
68
|
+
"icon_for_app",
|
|
69
|
+
"icon_for_model",
|
|
70
|
+
"install",
|
|
71
|
+
"model_menu_key",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Names are resolved lazily (PEP 562) instead of imported eagerly here.
|
|
75
|
+
# Django imports this top-level package during app-registry population
|
|
76
|
+
# (phase 1, before any app's `models` module may be imported) just to
|
|
77
|
+
# discover this app's AppConfig — an eager `from .models import ...` (or
|
|
78
|
+
# anything that transitively imports models, like `install`/`views`) at
|
|
79
|
+
# that point raises AppRegistryNotReady.
|
|
80
|
+
_LAZY_ATTRS = {
|
|
81
|
+
"install": "django_admin_home.install",
|
|
82
|
+
"MenuAccess": "django_admin_home.models",
|
|
83
|
+
"MenuFavorite": "django_admin_home.models",
|
|
84
|
+
"build_custom_pages_group": "django_admin_home.pages",
|
|
85
|
+
"DEFAULT_APP_ICON": "django_admin_home.menu",
|
|
86
|
+
"DEFAULT_MODEL_ICON": "django_admin_home.menu",
|
|
87
|
+
"HOME_ICON": "django_admin_home.menu",
|
|
88
|
+
"app_menu_key": "django_admin_home.menu",
|
|
89
|
+
"build_menu_tree": "django_admin_home.menu",
|
|
90
|
+
"flatten_menu_items": "django_admin_home.menu",
|
|
91
|
+
"icon_for_app": "django_admin_home.menu",
|
|
92
|
+
"icon_for_model": "django_admin_home.menu",
|
|
93
|
+
"model_menu_key": "django_admin_home.menu",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def __getattr__(name: str):
|
|
98
|
+
module_path = _LAZY_ATTRS.get(name)
|
|
99
|
+
if module_path is None:
|
|
100
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
101
|
+
import importlib
|
|
102
|
+
|
|
103
|
+
module = importlib.import_module(module_path)
|
|
104
|
+
value = getattr(module, name)
|
|
105
|
+
globals()[name] = value # cache: subsequent access skips __getattr__
|
|
106
|
+
return value
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Installs the sidebar navigation + home dashboard on an ``AdminSite``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.contrib import admin
|
|
7
|
+
from django.urls import path
|
|
8
|
+
|
|
9
|
+
from django_admin_home.menu import build_menu_tree, flatten_menu_items
|
|
10
|
+
from django_admin_home.pages import build_custom_pages_group
|
|
11
|
+
from django_admin_home.views import toggle_favorite, track_access
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _max_most_accessed() -> int:
|
|
15
|
+
return getattr(settings, "ADMIN_HOME_MAX_MOST_ACCESSED", 8)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _favorite_keys(user):
|
|
19
|
+
from django_admin_home.models import MenuFavorite
|
|
20
|
+
|
|
21
|
+
return set(MenuFavorite.objects.filter(user=user).values_list("menu_key", flat=True))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _menu_tree_for_request(site, request):
|
|
25
|
+
"""Builds the menu tree honouring permissions (via ``get_app_list``)."""
|
|
26
|
+
try:
|
|
27
|
+
app_list = site.get_app_list(request)
|
|
28
|
+
except Exception:
|
|
29
|
+
app_list = []
|
|
30
|
+
favorites = _favorite_keys(request.user) if request.user.is_authenticated else set()
|
|
31
|
+
menu_tree = build_menu_tree(app_list, favorites=favorites)
|
|
32
|
+
|
|
33
|
+
# Appends the "Pages" group (custom, non-model links), respecting
|
|
34
|
+
# permissions. Additive and defensive: any failure here must never
|
|
35
|
+
# prevent the rest of the menu from being built.
|
|
36
|
+
try:
|
|
37
|
+
pages_group = build_custom_pages_group(request.user, favorites=favorites)
|
|
38
|
+
if pages_group:
|
|
39
|
+
menu_tree = menu_tree + [pages_group]
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
return menu_tree, favorites
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _most_accessed(user, menu_index):
|
|
47
|
+
"""Top items accessed by the user that still exist in the navigation."""
|
|
48
|
+
from django_admin_home.models import MenuAccess
|
|
49
|
+
|
|
50
|
+
rows = (
|
|
51
|
+
MenuAccess.objects.filter(user=user)
|
|
52
|
+
.order_by("-access_count", "-last_access")
|
|
53
|
+
.values("menu_key", "access_count")
|
|
54
|
+
)
|
|
55
|
+
cards = []
|
|
56
|
+
max_items = _max_most_accessed()
|
|
57
|
+
for row in rows:
|
|
58
|
+
meta = menu_index.get(row["menu_key"])
|
|
59
|
+
if not meta or not meta.get("url"):
|
|
60
|
+
continue
|
|
61
|
+
cards.append({**meta, "access_count": row["access_count"]})
|
|
62
|
+
if len(cards) >= max_items:
|
|
63
|
+
break
|
|
64
|
+
return cards
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _favorites_cards(menu_index, favorites):
|
|
68
|
+
cards = []
|
|
69
|
+
for key in favorites:
|
|
70
|
+
meta = menu_index.get(key)
|
|
71
|
+
if meta and meta.get("url"):
|
|
72
|
+
cards.append(meta)
|
|
73
|
+
cards.sort(key=lambda c: (c["name"] or "").lower())
|
|
74
|
+
return cards
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def install(site: admin.AdminSite | None = None) -> None:
|
|
78
|
+
"""Applies the sidebar + home dashboard customization to ``site``.
|
|
79
|
+
|
|
80
|
+
Idempotent (safe to call more than once) and additive: wraps
|
|
81
|
+
``each_context``/``index``/``get_urls``, preserving the originals, and
|
|
82
|
+
never touches ``site_header``/``site_title``/``index_title`` or
|
|
83
|
+
``has_permission`` — those stay a project-level concern.
|
|
84
|
+
|
|
85
|
+
Call this once, for example from your own app's ``AppConfig.ready()``::
|
|
86
|
+
|
|
87
|
+
from django_admin_home import install
|
|
88
|
+
|
|
89
|
+
class MyAppConfig(AppConfig):
|
|
90
|
+
def ready(self):
|
|
91
|
+
install()
|
|
92
|
+
"""
|
|
93
|
+
site = site or admin.site
|
|
94
|
+
|
|
95
|
+
if getattr(site, "_admin_home_installed", False):
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
original_each_context = site.each_context
|
|
99
|
+
original_index = site.index
|
|
100
|
+
original_get_urls = site.get_urls
|
|
101
|
+
|
|
102
|
+
def each_context(request):
|
|
103
|
+
context = original_each_context(request)
|
|
104
|
+
try:
|
|
105
|
+
menu_tree, favorites = _menu_tree_for_request(site, request)
|
|
106
|
+
context["admin_home_menu_tree"] = menu_tree
|
|
107
|
+
context["admin_home_menu_favorites"] = _favorites_cards(flatten_menu_items(menu_tree), favorites)
|
|
108
|
+
except Exception:
|
|
109
|
+
# The sidebar is additive; it must never break the admin.
|
|
110
|
+
context.setdefault("admin_home_menu_tree", [])
|
|
111
|
+
context.setdefault("admin_home_menu_favorites", [])
|
|
112
|
+
return context
|
|
113
|
+
|
|
114
|
+
def index(request, extra_context=None):
|
|
115
|
+
extra_context = extra_context or {}
|
|
116
|
+
try:
|
|
117
|
+
menu_tree, favorites = _menu_tree_for_request(site, request)
|
|
118
|
+
menu_index = flatten_menu_items(menu_tree)
|
|
119
|
+
extra_context["admin_home_most_accessed"] = _most_accessed(request.user, menu_index)
|
|
120
|
+
extra_context["admin_home_favorite_cards"] = _favorites_cards(menu_index, favorites)
|
|
121
|
+
except Exception:
|
|
122
|
+
extra_context.setdefault("admin_home_most_accessed", [])
|
|
123
|
+
extra_context.setdefault("admin_home_favorite_cards", [])
|
|
124
|
+
return original_index(request, extra_context)
|
|
125
|
+
|
|
126
|
+
def get_urls():
|
|
127
|
+
custom = [
|
|
128
|
+
path(
|
|
129
|
+
"menu/toggle-favorite/",
|
|
130
|
+
site.admin_view(toggle_favorite),
|
|
131
|
+
name="admin_home_toggle_favorite",
|
|
132
|
+
),
|
|
133
|
+
path(
|
|
134
|
+
"menu/track-access/",
|
|
135
|
+
site.admin_view(track_access),
|
|
136
|
+
name="admin_home_track_access",
|
|
137
|
+
),
|
|
138
|
+
]
|
|
139
|
+
return custom + original_get_urls()
|
|
140
|
+
|
|
141
|
+
site.each_context = each_context
|
|
142
|
+
site.index = index
|
|
143
|
+
site.get_urls = get_urls
|
|
144
|
+
|
|
145
|
+
site._admin_home_installed = True
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Builds the navigable menu tree from the admin's own ``app_list``.
|
|
2
|
+
|
|
3
|
+
The tree is derived from ``AdminSite.get_app_list(request)``, which already
|
|
4
|
+
respects the current user's permissions — this module only enriches each
|
|
5
|
+
app/model with an icon and a stable key, it never decides visibility.
|
|
6
|
+
|
|
7
|
+
Icons are referenced by *name* (a ``<symbol id="i-<name}">`` in the bundled
|
|
8
|
+
SVG sprite, see ``admin_home/_icon_sprite.html``), not by external font/CDN
|
|
9
|
+
classes, so the navigation keeps working offline. Any app/model without a
|
|
10
|
+
specific mapping falls back to a generic icon, so navigation keeps working
|
|
11
|
+
as new apps/models are added.
|
|
12
|
+
|
|
13
|
+
The "stable key" of each item (``menu_key``) is used both for favorites and
|
|
14
|
+
for the access counter:
|
|
15
|
+
|
|
16
|
+
- Model item: ``<app_label>.<object_name_lower>`` e.g. ``auth.user``
|
|
17
|
+
- App group: ``app.<app_label>`` e.g. ``app.auth``
|
|
18
|
+
- Home: ``home``
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from django.conf import settings
|
|
24
|
+
|
|
25
|
+
DEFAULT_APP_ICON = "folder"
|
|
26
|
+
DEFAULT_MODEL_ICON = "list"
|
|
27
|
+
HOME_ICON = "home"
|
|
28
|
+
|
|
29
|
+
# Minimal built-in fallback so a stock Django project (auth app) already
|
|
30
|
+
# gets sensible icons. Projects override/extend via ADMIN_HOME_APP_ICONS /
|
|
31
|
+
# ADMIN_HOME_MODEL_ICONS.
|
|
32
|
+
_BUILTIN_APP_ICONS = {"auth": "shield"}
|
|
33
|
+
_BUILTIN_MODEL_ICONS = {"auth.user": "user", "auth.group": "users"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _app_icons() -> dict:
|
|
37
|
+
icons = dict(_BUILTIN_APP_ICONS)
|
|
38
|
+
icons.update(getattr(settings, "ADMIN_HOME_APP_ICONS", None) or {})
|
|
39
|
+
return icons
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _model_icons() -> dict:
|
|
43
|
+
icons = dict(_BUILTIN_MODEL_ICONS)
|
|
44
|
+
icons.update(getattr(settings, "ADMIN_HOME_MODEL_ICONS", None) or {})
|
|
45
|
+
return icons
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def model_menu_key(app_label: str, object_name: str) -> str:
|
|
49
|
+
"""Stable key for a model item."""
|
|
50
|
+
return f"{app_label}.{object_name}".lower()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def app_menu_key(app_label: str) -> str:
|
|
54
|
+
return f"app.{app_label}".lower()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def icon_for_app(app_label: str) -> str:
|
|
58
|
+
return _app_icons().get((app_label or "").lower(), DEFAULT_APP_ICON)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def icon_for_model(app_label: str, object_name: str) -> str:
|
|
62
|
+
return _model_icons().get(model_menu_key(app_label, object_name), DEFAULT_MODEL_ICON)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_menu_tree(app_list, favorites: set[str] | None = None) -> list[dict]:
|
|
66
|
+
"""Turns the admin's ``app_list`` into a navigable tree.
|
|
67
|
+
|
|
68
|
+
Each app node: ``{key, name, icon, url, models: [...], is_favorite}``.
|
|
69
|
+
Each model node: ``{key, name, icon, url, add_url, is_favorite}``.
|
|
70
|
+
|
|
71
|
+
``favorites`` is the set of ``menu_key`` the current user favorited.
|
|
72
|
+
"""
|
|
73
|
+
favorites = favorites or set()
|
|
74
|
+
tree = []
|
|
75
|
+
for app in app_list:
|
|
76
|
+
app_label = app.get("app_label") or ""
|
|
77
|
+
app_key = app_menu_key(app_label)
|
|
78
|
+
models = []
|
|
79
|
+
for model in app.get("models", []):
|
|
80
|
+
object_name = (model.get("object_name") or model.get("name") or "").strip()
|
|
81
|
+
key = model_menu_key(app_label, object_name)
|
|
82
|
+
models.append(
|
|
83
|
+
{
|
|
84
|
+
"key": key,
|
|
85
|
+
"name": model.get("name"),
|
|
86
|
+
"icon": icon_for_model(app_label, object_name),
|
|
87
|
+
"url": model.get("admin_url"),
|
|
88
|
+
"add_url": model.get("add_url"),
|
|
89
|
+
"is_favorite": key in favorites,
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
tree.append(
|
|
93
|
+
{
|
|
94
|
+
"key": app_key,
|
|
95
|
+
"name": app.get("name"),
|
|
96
|
+
"icon": icon_for_app(app_label),
|
|
97
|
+
"url": app.get("app_url"),
|
|
98
|
+
"models": models,
|
|
99
|
+
"is_favorite": app_key in favorites,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
return tree
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def flatten_menu_items(menu_tree) -> dict:
|
|
106
|
+
"""Index ``menu_key -> metadata`` (apps + models).
|
|
107
|
+
|
|
108
|
+
Used to resolve "most accessed" and "favorites" cards from the
|
|
109
|
+
persisted keys, keeping name/icon/url in sync with the live menu.
|
|
110
|
+
"""
|
|
111
|
+
index: dict = {}
|
|
112
|
+
for app in menu_tree:
|
|
113
|
+
index[app["key"]] = {
|
|
114
|
+
"key": app["key"],
|
|
115
|
+
"name": app["name"],
|
|
116
|
+
"icon": app["icon"],
|
|
117
|
+
"url": app["url"],
|
|
118
|
+
"parent": None,
|
|
119
|
+
}
|
|
120
|
+
for model in app["models"]:
|
|
121
|
+
index[model["key"]] = {
|
|
122
|
+
"key": model["key"],
|
|
123
|
+
"name": model["name"],
|
|
124
|
+
"icon": model["icon"],
|
|
125
|
+
"url": model["url"],
|
|
126
|
+
"parent": app["name"],
|
|
127
|
+
"new_tab": bool(model.get("new_tab")),
|
|
128
|
+
}
|
|
129
|
+
return index
|