Skip to content

Commit e5fafe2

Browse files
author
David Farrington
committed
Allow generation of custom menu from mapping of app/name -> models
1 parent 6a10171 commit e5fafe2

File tree

5 files changed

+73
-81
lines changed

5 files changed

+73
-81
lines changed

jazzmin/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
"order_with_respect_to": [],
5151
# Custom links to append to side menu app groups, keyed on app name
5252
"custom_links": {},
53+
# Do not generate a menu based off of installed apps, instead, manually craft one using this app -> model mapping
54+
"custom_menu": {},
5355
# Custom icons for side menu apps/models See https://fontawesome.com/icons?d=gallery&m=free
5456
# for a list of icon classes
5557
"icons": {"auth": "fas fa-users-cog", "auth.user": "fas fa-user", "auth.Group": "fas fa-users"},

jazzmin/templatetags/jazzmin.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
get_admin_url,
3232
make_menu,
3333
has_fieldsets_check,
34+
regroup_available_apps,
3435
)
3536

3637
User = get_user_model()
@@ -49,18 +50,22 @@ def get_side_menu(context: Context, using: str = "available_apps") -> List[Dict]
4950
if not user:
5051
return []
5152

53+
menu = []
5254
options = get_settings()
5355
ordering = options.get("order_with_respect_to", [])
5456
ordering = [x.lower() for x in ordering]
5557

56-
menu = []
5758
available_apps = copy.deepcopy(context.get(using, []))
5859

5960
custom_links = {
6061
app_name: make_menu(user, links, options, allow_appmenus=False)
6162
for app_name, links in options.get("custom_links", {}).items()
6263
}
6364

65+
# If we are using custom grouping, overwrite available_apps based on our grouping
66+
if options.get("custom_menu") and options["custom_menu"]:
67+
available_apps = regroup_available_apps(available_apps, options["custom_menu"])
68+
6469
for app in available_apps:
6570
app_label = app["app_label"].lower()
6671
app_custom_links = custom_links.get(app_label, [])
@@ -83,18 +88,13 @@ def get_side_menu(context: Context, using: str = "available_apps") -> List[Dict]
8388

8489
custom_link_names = [x.get("name", "").lower() for x in app_custom_links]
8590
model_ordering = list(
86-
filter(
87-
lambda x: x.lower().startswith("{}.".format(app_label)) or x.lower() in custom_link_names,
88-
ordering,
89-
)
91+
filter(lambda x: x.lower().startswith("{}.".format(app_label)) or x.lower() in custom_link_names, ordering,)
9092
)
9193

9294
if len(menu_items):
9395
if model_ordering:
9496
menu_items = order_with_respect_to(
95-
menu_items,
96-
model_ordering,
97-
getter=lambda x: x.get("model_str", x.get("name", "").lower()),
97+
menu_items, model_ordering, getter=lambda x: x.get("model_str", x.get("name", "").lower()),
9898
)
9999
app["models"] = menu_items
100100
menu.append(app)
@@ -420,8 +420,7 @@ def deleted(x: str) -> Dict:
420420

421421
elif "changed" in sub_message:
422422
sub_message["changed"]["fields"] = get_text_list(
423-
[gettext(field_name) for field_name in sub_message["changed"]["fields"]],
424-
gettext("and"),
423+
[gettext(field_name) for field_name in sub_message["changed"]["fields"]], gettext("and"),
425424
)
426425
if "name" in sub_message["changed"]:
427426
sub_message["changed"]["name"] = gettext(sub_message["changed"]["name"])

jazzmin/utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,32 @@ def has_fieldsets_check(adminform: AdminForm) -> bool:
232232
if not fieldsets or (len(fieldsets) == 1 and fieldsets[0][0] is None):
233233
return False
234234
return True
235+
236+
237+
def regroup_available_apps(available_apps: List[Dict], grouping: Dict[str, List[str]]) -> List[Dict]:
238+
# Make a list of all apps, and all models, keyed on app name or model name
239+
all_models, all_apps = {}, {}
240+
for app in available_apps:
241+
app_label = app["app_label"].lower()
242+
all_apps[app_label] = app
243+
for model in app["models"]:
244+
model_name = model["object_name"].lower()
245+
all_models[app_label + "." + model_name] = model.copy()
246+
247+
# Start overwriting available_apps
248+
new_available_apps = []
249+
for group, children in grouping.items():
250+
app = all_apps.get(
251+
group.lower(),
252+
{"name": group.title(), "app_label": group, "app_url": None, "has_module_perms": True, "models": []},
253+
)
254+
255+
app["models"] = []
256+
for model in children:
257+
model_obj = all_models.get(model.lower())
258+
if model_obj:
259+
app["models"].append(model_obj)
260+
261+
new_available_apps.append(app)
262+
263+
return new_available_apps

tests/test_app/library/settings.py

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,15 @@
7171

7272
DATABASES = {
7373
"default": dj_database_url.config(
74-
env="DATABASE_URL",
75-
conn_max_age=500,
76-
default="sqlite:///{}".format(os.path.join(BASE_DIR, "db.sqlite3")),
74+
env="DATABASE_URL", conn_max_age=500, default="sqlite:///{}".format(os.path.join(BASE_DIR, "db.sqlite3")),
7775
)
7876
}
7977

8078
AUTH_PASSWORD_VALIDATORS = [
81-
{
82-
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
83-
},
84-
{
85-
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
86-
},
87-
{
88-
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
89-
},
90-
{
91-
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
92-
},
79+
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",},
80+
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
81+
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
82+
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
9383
]
9484

9585
LANGUAGE_CODE = "en"
@@ -144,11 +134,7 @@
144134
# Url that gets reversed (Permissions can be added)
145135
{"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]},
146136
# external url that opens in a new window (Permissions can be added)
147-
{
148-
"name": "Support",
149-
"url": "https://github.com/farridav/django-jazzmin/issues",
150-
"new_window": True,
151-
},
137+
{"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True,},
152138
# model admin to link to (Permissions checked against model)
153139
{"model": "auth.User"},
154140
# App with dropdown menu to all its models pages (Permissions checked against models)
@@ -160,11 +146,7 @@
160146
#############
161147
# Additional links to include in the user menu on the top right ('app' url type is not allowed)
162148
"usermenu_links": [
163-
{
164-
"name": "Support",
165-
"url": "https://github.com/farridav/django-jazzmin/issues",
166-
"new_window": True,
167-
},
149+
{"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True,},
168150
{"model": "auth.user"},
169151
],
170152
#############
@@ -191,6 +173,8 @@
191173
}
192174
]
193175
},
176+
# Dont generate a menu based off installed apps, instead, craft one using this app/arbitrary name -> model mapping
177+
"custom_menu": {},
194178
# Custom icons for side menu apps/models See https://fontawesome.com/icons?d=gallery&m=free
195179
# for a list of icon classes
196180
"icons": {

tests/test_jazzmin_menus.py

Lines changed: 24 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import pytest
2+
from django.contrib.admin import site
3+
from django.template import Context
24
from django.urls import reverse
35

6+
from jazzmin.templatetags.jazzmin import get_side_menu
47
from .test_app.library.factories import UserFactory
58
from .utils import (
69
parse_sidemenu,
@@ -21,21 +24,10 @@ def test_side_menu(admin_client, settings):
2124

2225
assert parse_sidemenu(response) == {
2326
"Administration": ["/en/admin/admin/logentry/"],
24-
"Authentication and Authorization": [
25-
"/en/admin/auth/group/",
26-
"/en/admin/auth/user/",
27-
],
28-
"Books": [
29-
"/en/admin/books/author/",
30-
"/en/admin/books/book/",
31-
"/en/admin/books/genre/",
32-
],
27+
"Authentication and Authorization": ["/en/admin/auth/group/", "/en/admin/auth/user/",],
28+
"Books": ["/en/admin/books/author/", "/en/admin/books/book/", "/en/admin/books/genre/",],
3329
"Global": ["/en/admin/"],
34-
"Loans": [
35-
"/make_messages/",
36-
"/en/admin/loans/bookloan/",
37-
"/en/admin/loans/library/",
38-
],
30+
"Loans": ["/make_messages/", "/en/admin/loans/bookloan/", "/en/admin/loans/library/",],
3931
}
4032

4133
settings.JAZZMIN_SETTINGS = override_jazzmin_settings(hide_models=["auth.user"])
@@ -44,16 +36,8 @@ def test_side_menu(admin_client, settings):
4436
assert parse_sidemenu(response) == {
4537
"Global": ["/en/admin/"],
4638
"Authentication and Authorization": ["/en/admin/auth/group/"],
47-
"Books": [
48-
"/en/admin/books/author/",
49-
"/en/admin/books/book/",
50-
"/en/admin/books/genre/",
51-
],
52-
"Loans": [
53-
"/make_messages/",
54-
"/en/admin/loans/bookloan/",
55-
"/en/admin/loans/library/",
56-
],
39+
"Books": ["/en/admin/books/author/", "/en/admin/books/book/", "/en/admin/books/genre/",],
40+
"Loans": ["/make_messages/", "/en/admin/loans/bookloan/", "/en/admin/loans/library/",],
5741
"Administration": ["/en/admin/admin/logentry/"],
5842
}
5943

@@ -103,11 +87,7 @@ def test_top_menu(admin_client, settings):
10387
settings.JAZZMIN_SETTINGS = override_jazzmin_settings(
10488
topmenu_links=[
10589
{"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]},
106-
{
107-
"name": "Support",
108-
"url": "https://github.com/farridav/django-jazzmin/issues",
109-
"new_window": True,
110-
},
90+
{"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True,},
11191
{"model": "auth.User"},
11292
{"app": "books"},
11393
]
@@ -117,10 +97,7 @@ def test_top_menu(admin_client, settings):
11797

11898
assert parse_topmenu(response) == [
11999
{"name": "Home", "link": "/en/admin/"},
120-
{
121-
"name": "Support",
122-
"link": "https://github.com/farridav/django-jazzmin/issues",
123-
},
100+
{"name": "Support", "link": "https://github.com/farridav/django-jazzmin/issues",},
124101
{"name": "Users", "link": "/en/admin/auth/user/"},
125102
{
126103
"name": "Books",
@@ -144,11 +121,7 @@ def test_user_menu(admin_user, client, settings):
144121
settings.JAZZMIN_SETTINGS = override_jazzmin_settings(
145122
usermenu_links=[
146123
{"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]},
147-
{
148-
"name": "Support",
149-
"url": "https://github.com/farridav/django-jazzmin/issues",
150-
"new_window": True,
151-
},
124+
{"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True},
152125
{"model": "auth.User"},
153126
]
154127
)
@@ -160,13 +133,18 @@ def test_user_menu(admin_user, client, settings):
160133
{"link": "/en/admin/password_change/", "name": "Change password"},
161134
{"link": "/en/admin/logout/", "name": "Log out"},
162135
{"link": "/en/admin/", "name": "Home"},
163-
{
164-
"link": "https://github.com/farridav/django-jazzmin/issues",
165-
"name": "Support",
166-
},
136+
{"link": "https://github.com/farridav/django-jazzmin/issues", "name": "Support"},
167137
{"link": "/en/admin/auth/user/", "name": "Users"},
168-
{
169-
"link": "/en/admin/auth/user/{}/change/".format(admin_user.pk),
170-
"name": "See Profile",
171-
},
138+
{"link": "/en/admin/auth/user/{}/change/".format(admin_user.pk), "name": "See Profile"},
172139
]
140+
141+
142+
def test_custom_menu_grouping(settings, rf):
143+
context = site.each_context(rf)
144+
settings.JAZZMIN_SETTINGS = override_jazzmin_settings(
145+
custom_menu={"auth": ["books.book"], "arbitrary name": ["auth.user", "auth.group"]}
146+
)
147+
148+
menu = get_side_menu(context)
149+
150+
assert menu == []

0 commit comments

Comments
 (0)