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.
Files changed (31) hide show
  1. django_admin_home-0.1.0/LICENSE +21 -0
  2. django_admin_home-0.1.0/PKG-INFO +142 -0
  3. django_admin_home-0.1.0/README.md +109 -0
  4. django_admin_home-0.1.0/django_admin_home/__init__.py +106 -0
  5. django_admin_home-0.1.0/django_admin_home/apps.py +10 -0
  6. django_admin_home-0.1.0/django_admin_home/install.py +145 -0
  7. django_admin_home-0.1.0/django_admin_home/menu.py +129 -0
  8. django_admin_home-0.1.0/django_admin_home/migrations/0001_initial.py +64 -0
  9. django_admin_home-0.1.0/django_admin_home/migrations/__init__.py +0 -0
  10. django_admin_home-0.1.0/django_admin_home/models.py +45 -0
  11. django_admin_home-0.1.0/django_admin_home/pages.py +108 -0
  12. django_admin_home-0.1.0/django_admin_home/static/django_admin_home/css/home.css +332 -0
  13. django_admin_home-0.1.0/django_admin_home/static/django_admin_home/css/nav.css +371 -0
  14. django_admin_home-0.1.0/django_admin_home/static/django_admin_home/js/nav.js +331 -0
  15. django_admin_home-0.1.0/django_admin_home/templates/admin/index.html +124 -0
  16. django_admin_home-0.1.0/django_admin_home/templates/admin/nav_sidebar.html +88 -0
  17. django_admin_home-0.1.0/django_admin_home/templates/admin_home/_brand.html +6 -0
  18. django_admin_home-0.1.0/django_admin_home/templates/admin_home/_icon_sprite.html +29 -0
  19. django_admin_home-0.1.0/django_admin_home/views.py +34 -0
  20. django_admin_home-0.1.0/django_admin_home.egg-info/PKG-INFO +142 -0
  21. django_admin_home-0.1.0/django_admin_home.egg-info/SOURCES.txt +29 -0
  22. django_admin_home-0.1.0/django_admin_home.egg-info/dependency_links.txt +1 -0
  23. django_admin_home-0.1.0/django_admin_home.egg-info/requires.txt +5 -0
  24. django_admin_home-0.1.0/django_admin_home.egg-info/top_level.txt +1 -0
  25. django_admin_home-0.1.0/pyproject.toml +80 -0
  26. django_admin_home-0.1.0/setup.cfg +4 -0
  27. django_admin_home-0.1.0/tests/test_install.py +82 -0
  28. django_admin_home-0.1.0/tests/test_menu.py +101 -0
  29. django_admin_home-0.1.0/tests/test_pages.py +64 -0
  30. django_admin_home-0.1.0/tests/test_public_api.py +11 -0
  31. 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,10 @@
1
+ from __future__ import annotations
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class AdminHomeConfig(AppConfig):
7
+ default_auto_field = "django.db.models.BigAutoField"
8
+ name = "django_admin_home"
9
+ label = "admin_home"
10
+ verbose_name = "Admin Home Dashboard"
@@ -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