Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom menu definition support #241

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,9 +370,9 @@ Add a template for your model on your main template directory,
e.g [app/templates/admin/app_name/model_name/submit_line.html](https://github.com/farridav/django-jazzmin/tree/main/tests/test_app/library/books/templates/admin/loans/bookloan/submit_line.html)

```djangotemplate
{# extends "admin/submit_line.html" #}
{#% extends "admin/submit_line.html" %#}

{# block extra-actions #}
{#% block extra-actions %#}

{# For a simple link #}
<div class="form-group">
Expand All @@ -383,7 +383,7 @@ e.g [app/templates/admin/app_name/model_name/submit_line.html](https://github.co
<div class="form-group">
<input type="submit" class="btn btn-outline-info form-control" value="SomeAction" name="_your_action">
</div>
{# endblock #}
{#% endblock %#}
```

If you are adding a button that needs processing with the form, e.g (Save and send) you will need to add the
Expand Down
1 change: 1 addition & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Then get setup with `poetry install`

git clone [email protected]:farridav/django-jazzmin.git
poetry install
poetry shell

## Running the test project

Expand Down
45 changes: 45 additions & 0 deletions docs/ui_customisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,48 @@ Or to target your `dark_mode_theme` wrap it like this:
}
}
```

## Custom Menu

The custom menu feature allows you to manually craft the side menu using an app-to-model mapping instead of generating a menu based on installed apps. This provides more control over the structure and organization of the admin interface.

### Configuration

To enable and configure the custom menu, follow these steps:

1. **Enable Custom Menu in Settings**
Add the `custom_menu` setting in your `jazzmin/settings.py` file. This setting should be a dictionary where the keys are app labels or arbitrary group names, and the values are lists of model names.

Example configuration:
```python
JAZZMIN_SETTINGS = {
# other settings...

# Do not generate a menu based off of installed apps, instead manually craft one using this app -> model mapping
"custom_menu": {
"auth": ["books.book"], # Group 'auth' with model 'books.book'
"arbitrary_name": ["auth.user", "auth.group"] # Custom group with 'auth.user' and 'auth.group'
},

# other settings...
}
```

2. **Customize Menu Appearance**
You can also customize the icons for your apps and models using the `icons` setting. This allows for a visually distinct menu.

Example configuration:
```python
JAZZMIN_SETTINGS = {
# other settings...

# Custom icons for side menu apps/models
"icons": {
# other settings...
"arbitrary_name.user": "fas fa-user",
"arbitrary_name.group": "fas fa-users"
},

# other settings...
}
```
4 changes: 4 additions & 0 deletions jazzmin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
############
# Links to put along the nav bar
"topmenu_links": [],
# Whether or not we use icons on the top menu
"topmenu_icons": False,
#############
# User Menu #
#############
Expand All @@ -60,6 +62,8 @@
# Custom links to append to side menu app groups, keyed on lower case app label
# or makes a new group if the given app label doesnt exist in installed apps
"custom_links": {},
# Do not generate a menu based off of installed apps, instead, manually craft one using this app -> model mapping
"custom_menu": {},
# Custom icons for side menu apps/models See the link below
# https://fontawesome.com/icons?d=gallery&m=free&v=5.0.0,5.0.1,5.0.10,5.0.11,5.0.12,5.0.13,5.0.2,5.0.3,5.0.4,5.0.5,5.0.6,5.0.7,5.0.8,5.0.9,5.1.0,
# 5.1.1,5.2.0,5.3.0,5.3.1,5.4.0,5.4.1,5.4.2,5.13.0,5.12.0,
Expand Down
10 changes: 7 additions & 3 deletions jazzmin/templates/admin/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,19 @@
<li class="nav-item d-none d-sm-inline-block{% if link.children %} dropdown{% endif %}">
{% if link.children %}
<a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ link.name }}
{% if jazzmin_settings.topmenu_icons and link.icon %}<i class="fa-sm {{ link.icon }}">&nbsp;</i> {% endif %}{{ link.name }}
</a>
<div class="dropdown-menu">
{% for child in link.children %}
<a class="dropdown-item" href="{{ child.url }}" {% if link.new_window %}target="_blank"{% endif %}>{{ child.name }}</a>
<a class="dropdown-item" href="{{ child.url }}" {% if link.new_window %}target="_blank"{% endif %}>
{% if jazzmin_settings.topmenu_icons and child.icon %}<i class="fa-sm {{ child.icon }}">&nbsp;</i> {% endif %}{{ child.name }}
</a>
{% endfor %}
</div>
{% else %}
<a href="{{ link.url }}" class="nav-link" {% if link.new_window %}target="_blank"{% endif %}>{{ link.name }}</a>
<a href="{{ link.url }}" class="nav-link" {% if link.new_window %}target="_blank"{% endif %}>
{% if jazzmin_settings.topmenu_icons and link.icon %}<i class="fa-sm {{ link.icon }}">&nbsp;</i> {% endif %}{{ link.name }}
</a>
{% endif %}
</li>
{% endfor %}
Expand Down
50 changes: 32 additions & 18 deletions jazzmin/templatetags/jazzmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,34 @@
has_fieldsets_check,
make_menu,
order_with_respect_to,
regroup_apps,
)

User = get_user_model()
register = Library()
logger = logging.getLogger(__name__)


def _process_models(app_label: str, models: List[Dict], options: Dict) -> List[Dict]:
Copy link
Collaborator

@katiam2 katiam2 Dec 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a simple extraction to satisfy ruff requirements. Tests were added to the original message first and then the refactoring followed.

"""Process and filter out hidden models for a given app label."""
processed_models = []
for model in models:
model_str = f"{app_label}.{model['object_name']}".lower()
if model_str in options.get("hide_models", []):
continue

processed_model = model.copy()
processed_model.update(
{
"url": model["admin_url"],
"model_str": model_str,
"icon": options["icons"].get(model_str, options["default_icon_children"]),
}
)
processed_models.append(processed_model)
return processed_models


@register.simple_tag(takes_context=True)
def get_side_menu(context: Context, using: str = "available_apps") -> List[Dict]:
"""
Expand All @@ -53,15 +74,12 @@ def get_side_menu(context: Context, using: str = "available_apps") -> List[Dict]
if not user:
return []

menu = []
options = get_settings()
ordering = options.get("order_with_respect_to", [])
ordering = [x.lower() for x in ordering]

ordering = [x.lower() for x in options.get("order_with_respect_to", [])]
installed_apps = get_installed_apps()
available_apps: list[dict[str, Any]] = copy.deepcopy(context.get(using, []))

menu = []

# Add any arbitrary groups that are not in available_apps
for app_label in options.get("custom_links", {}):
if app_label.lower() not in installed_apps:
Expand All @@ -74,24 +92,20 @@ def get_side_menu(context: Context, using: str = "available_apps") -> List[Dict]
for app_name, links in options.get("custom_links", {}).items()
}

# Handle custom grouping
if options.get("custom_menu"):
available_apps = regroup_apps(available_apps, options["custom_menu"])

for app in available_apps:
app_label = app["app_label"]
app_custom_links = custom_links.get(app_label, [])
app["icon"] = options["icons"].get(app_label, options["default_icon_parents"])
if app_label in options["hide_apps"]:
continue

menu_items = []
for model in app.get("models", []):
model_str = "{app_label}.{model}".format(app_label=app_label, model=model["object_name"]).lower()
if model_str in options.get("hide_models", []):
continue

model["url"] = model["admin_url"]
model["model_str"] = model_str
model["icon"] = options["icons"].get(model_str, options["default_icon_children"])
menu_items.append(model)
app["icon"] = options["icons"].get(app_label, options["default_icon_parents"])
app_custom_links = custom_links.get(app_label, [])

# Use the helper function to process models
menu_items = _process_models(app_label, app.get("models", []), options)
menu_items.extend(app_custom_links)

custom_link_names = [x.get("name", "").lower() for x in app_custom_links]
Expand Down Expand Up @@ -540,7 +554,7 @@ def style_bold_first_word(message: str) -> SafeText:
message_words = escape(message).split()

if not len(message_words):
return ""
return mark_safe("")

message_words[0] = "<strong>{}</strong>".format(message_words[0])

Expand Down
35 changes: 32 additions & 3 deletions jazzmin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def make_menu(
"url": get_custom_url(link["url"], admin_site=admin_site),
"children": None,
"new_window": link.get("new_window", False),
"icon": link.get("icon", options["default_icon_children"]),
"icon": link.get("icon"),
}
)

Expand All @@ -197,7 +197,7 @@ def make_menu(
"url": get_admin_url(link["model"], admin_site=admin_site),
"children": [],
"new_window": link.get("new_window", False),
"icon": options["icons"].get(link["model"], options["default_icon_children"]),
"icon": options["icons"].get(link["model"].lower()),
}
)

Expand All @@ -216,7 +216,7 @@ def make_menu(
"name": getattr(apps.app_configs[link["app"]], "verbose_name", link["app"]).title(),
"url": "#",
"children": children,
"icon": options["icons"].get(link["app"], options["default_icon_children"]),
"icon": options["icons"].get(link["app"].lower()),
}
)

Expand All @@ -239,5 +239,34 @@ def decorator(func: Callable):
return decorator


def regroup_apps(available_apps: List[Dict], grouping: Dict[str, List[str]]) -> List[Dict]:
# Make a list of all apps, and all models, keyed on app name or model name
all_models, all_apps = {}, {}
for app in available_apps:
app_label = app["app_label"].lower()
all_apps[app_label] = app
for model in app["models"]:
model_name = model["object_name"].lower()
all_models[app_label + "." + model_name] = model.copy()

# Start overwriting available_apps
new_available_apps = []
for group, children in grouping.items():
app = all_apps.get(
group.lower(),
{"name": group.title(), "app_label": group, "app_url": None, "has_module_perms": True, "models": []},
)

app["models"] = []
for model in children:
model_obj = all_models.get(model.lower())
if model_obj:
app["models"].append(model_obj)

new_available_apps.append(app)

return new_available_apps


def get_installed_apps() -> List[str]:
return [app_config.label for app_config in apps.get_app_configs()]
15 changes: 13 additions & 2 deletions tests/test_app/library/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,22 @@
# Links to put along the top menu
"topmenu_links": [
# Url that gets reversed (Permissions can be added)
{"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]},
{"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"], "icon": "fas fa-home"},
# external url that opens in a new window (Permissions can be added)
{"name": "Support", "url": "https://github.com/farridav/django-jazzmin/issues", "new_window": True},
{
"name": "Support",
"url": "https://github.com/farridav/django-jazzmin/issues",
"new_window": True,
"icon": "fas fa-cog",
},
# model admin to link to (Permissions checked against model)
{"model": "auth.User"},
# App with dropdown menu to all its models pages (Permissions checked against models)
{"app": "books"},
{"app": "loans"},
],
# Whether or not we use icons on the top menu
"topmenu_icons": True,
#############
# User Menu #
#############
Expand Down Expand Up @@ -184,6 +191,8 @@
{"name": "Custom View", "url": "admin:custom_view", "icon": "fas fa-box-open"},
]
},
# Dont generate a side menu from installed apps, instead, craft one using this app/arbitrary name -> model mapping
"custom_menu": {},
# Custom icons for side menu apps/models See the link below
# https://fontawesome.com/icons?d=gallery&m=free&v=5.0.0,5.0.1,5.0.10,5.0.11,5.0.12,5.0.13,5.0.2,5.0.3,5.0.4,5.0.5,5.0.6,5.0.7,5.0.8,5.0.9,5.1.0,
# 5.1.1,5.2.0,5.3.0,5.3.1,5.4.0,5.4.1,5.4.2,5.13.0,5.12.0,
Expand All @@ -194,9 +203,11 @@
"auth.user": "fas fa-user",
"auth.Group": "fas fa-users",
"admin.LogEntry": "fas fa-file",
"books": "fas fa-book",
"books.Author": "fas fa-user",
"books.Book": "fas fa-book",
"books.Genre": "fas fa-photo-video",
"loans": "fas fa-book-open",
"loans.BookLoan": "fas fa-book-open",
"loans.Library": "fas fa-book-reader",
},
Expand Down
Loading