import copy import logging from typing import Any, Dict from django.conf import settings from django.templatetags.static import static from .utils import get_admin_url, get_model_meta logger = logging.getLogger(__name__) DEFAULT_SETTINGS: Dict[str, Any] = { # title of the window (Will default to current_admin_site.site_title) "site_title": None, # Title on the login screen (19 chars max) (will default to current_admin_site.site_header) "site_header": None, # Title on the brand (19 chars max) (will default to current_admin_site.site_header) "site_brand": None, # Relative path to logo for your site, used for brand on top left (must be present in static files) "site_logo": "vendor/adminlte/img/AdminLTELogo.png", # Relative path to logo for your site, used for login logo (must be present in static files. Defaults to site_logo) "login_logo": None, # Logo to use for login form in dark themes (must be present in static files. Defaults to login_logo) "login_logo_dark": None, # CSS classes that are applied to the logo "site_logo_classes": "img-circle", # Relative path to a favicon for your site, will default to site_logo if absent (ideally 32x32 px) "site_icon": None, # Welcome text on the login screen "welcome_sign": "Welcome", # Copyright on the footer "copyright": "", # The model admin to search from the search bar, search bar omitted if excluded "search_model": None, # Field name on user model that contains avatar ImageField/URLField/Charfield or a callable that receives the user "user_avatar": None, ############ # Top Menu # ############ # Links to put along the nav bar "topmenu_links": [], ############# # User Menu # ############# # Additional links to include in the user menu on the top right ('app' url type is not allowed) "usermenu_links": [], ############# # Side Menu # ############# # Whether to display the side menu "show_sidebar": True, # Whether to aut expand the menu "navigation_expanded": True, # Hide these apps when generating side menu e.g (auth) "hide_apps": [], # Hide these models when generating side menu (e.g auth.user) "hide_models": [], # List of apps to base side menu ordering off of "order_with_respect_to": [], # Custom links to append to side menu app groups, keyed on app name "custom_links": {}, # 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, # 5.11.2,5.11.1,5.10.0,5.9.0,5.8.2,5.8.1,5.7.2,5.7.1,5.7.0,5.6.3,5.5.0,5.4.2 # for the full list of 5.13.0 free icon classes "icons": {"auth": "fas fa-users-cog", "auth.user": "fas fa-user", "auth.Group": "fas fa-users"}, # Icons that are used when one is not manually specified "default_icon_parents": "fas fa-chevron-circle-right", "default_icon_children": "fas fa-circle", ################# # Related Modal # ################# # Activate Bootstrap modal "related_modal_active": False, ############# # UI Tweaks # ############# # Relative paths to custom CSS/JS scripts (must be present in static files) "custom_css": None, "custom_js": None, # Whether to link font from fonts.googleapis.com (use custom_css to supply font otherwise) "use_google_fonts_cdn": True, # Whether to show the UI customizer on the sidebar "show_ui_builder": False, ############### # Change view # ############### # Render out the change view as a single form, or in tabs, current options are # - single # - horizontal_tabs (default) # - vertical_tabs # - collapsible # - carousel "changeform_format": "horizontal_tabs", # override change forms on a per modeladmin basis "changeform_format_overrides": {}, # Add a language dropdown into the admin "language_chooser": False, } ####################################### # Currently available UI tweaks # # Use the UI builder to generate this # ####################################### DEFAULT_UI_TWEAKS: Dict[str, Any] = { # Small text on the top navbar "navbar_small_text": False, # Small text on the footer "footer_small_text": False, # Small text everywhere "body_small_text": False, # Small text on the brand/logo "brand_small_text": False, # brand/logo background colour "brand_colour": False, # Link colour "accent": "accent-primary", # topmenu colour "navbar": "navbar-white navbar-light", # topmenu border "no_navbar_border": False, # Make the top navbar sticky, keeping it in view as you scroll "navbar_fixed": False, # Whether to constrain the page to a box (leaving big margins at the side) "layout_boxed": False, # Make the footer sticky, keeping it in view all the time "footer_fixed": False, # Make the sidebar sticky, keeping it in view as you scroll "sidebar_fixed": False, # sidemenu colour "sidebar": "sidebar-dark-primary", # sidemenu small text "sidebar_nav_small_text": False, # Disable expanding on hover of collapsed sidebar "sidebar_disable_expand": False, # Indent child menu items on sidebar "sidebar_nav_child_indent": False, # Use a compact sidebar "sidebar_nav_compact_style": False, # Use the AdminLTE2 style sidebar "sidebar_nav_legacy_style": False, # Use a flat style sidebar "sidebar_nav_flat_style": False, # Bootstrap theme to use (default, or from bootswatch, see THEMES below) "theme": "default", # Theme to use instead if the user has opted for dark mode (e.g darkly/cyborg/slate/solar/superhero) "dark_mode_theme": None, # The classes/styles to use with buttons "button_classes": { "primary": "btn-primary", "secondary": "btn-secondary", "info": "btn-info", "warning": "btn-warning", "danger": "btn-danger", "success": "btn-success", }, } THEMES = { # light themes "default": "vendor/bootswatch/default/bootstrap.min.css", "cerulean": "vendor/bootswatch/cerulean/bootstrap.min.css", "cosmo": "vendor/bootswatch/cosmo/bootstrap.min.css", "flatly": "vendor/bootswatch/flatly/bootstrap.min.css", "journal": "vendor/bootswatch/journal/bootstrap.min.css", "litera": "vendor/bootswatch/litera/bootstrap.min.css", "lumen": "vendor/bootswatch/lumen/bootstrap.min.css", "lux": "vendor/bootswatch/lux/bootstrap.min.css", "materia": "vendor/bootswatch/materia/bootstrap.min.css", "minty": "vendor/bootswatch/minty/bootstrap.min.css", "pulse": "vendor/bootswatch/pulse/bootstrap.min.css", "sandstone": "vendor/bootswatch/sandstone/bootstrap.min.css", "simplex": "vendor/bootswatch/simplex/bootstrap.min.css", "sketchy": "vendor/bootswatch/sketchy/bootstrap.min.css", "spacelab": "vendor/bootswatch/spacelab/bootstrap.min.css", "united": "vendor/bootswatch/united/bootstrap.min.css", "yeti": "vendor/bootswatch/yeti/bootstrap.min.css", # dark themes "darkly": "vendor/bootswatch/darkly/bootstrap.min.css", "cyborg": "vendor/bootswatch/cyborg/bootstrap.min.css", "slate": "vendor/bootswatch/slate/bootstrap.min.css", "solar": "vendor/bootswatch/solar/bootstrap.min.css", "superhero": "vendor/bootswatch/superhero/bootstrap.min.css", } DARK_THEMES = ("darkly", "cyborg", "slate", "solar", "superhero") CHANGEFORM_TEMPLATES = { "single": "jazzmin/includes/single.html", "carousel": "jazzmin/includes/carousel.html", "collapsible": "jazzmin/includes/collapsible.html", "horizontal_tabs": "jazzmin/includes/horizontal_tabs.html", "vertical_tabs": "jazzmin/includes/vertical_tabs.html", } def get_search_model_string(search_model: str) -> str: """ Get a search model string for reversing an admin url. Ensure the model name is lower cased but remain the app name untouched. """ app, model_name = search_model.split(".") return "{app}.{model_name}".format(app=app, model_name=model_name.lower()) def get_settings() -> Dict: jazzmin_settings = copy.deepcopy(DEFAULT_SETTINGS) user_settings = {x: y for x, y in getattr(settings, "JAZZMIN_SETTINGS", {}).items() if y is not None} jazzmin_settings.update(user_settings) # Extract search model configuration from search_model setting if jazzmin_settings["search_model"]: if not isinstance(jazzmin_settings["search_model"], list): jazzmin_settings["search_model"] = [jazzmin_settings["search_model"]] jazzmin_settings["search_models_parsed"] = [] for search_model in jazzmin_settings["search_model"]: jazzmin_search_model = {} jazzmin_search_model["search_url"] = get_admin_url(get_search_model_string(search_model)) model_meta = get_model_meta(search_model) if model_meta: jazzmin_search_model["search_name"] = model_meta.verbose_name_plural.title() else: jazzmin_search_model["search_name"] = search_model.split(".")[-1] + "s" jazzmin_settings["search_models_parsed"].append(jazzmin_search_model) # Deal with single strings in hide_apps/hide_models and make sure we lower case 'em if isinstance(jazzmin_settings["hide_apps"], str): jazzmin_settings["hide_apps"] = [jazzmin_settings["hide_apps"]] jazzmin_settings["hide_apps"] = [x.lower() for x in jazzmin_settings["hide_apps"]] if isinstance(jazzmin_settings["hide_models"], str): jazzmin_settings["hide_models"] = [jazzmin_settings["hide_models"]] jazzmin_settings["hide_models"] = [x.lower() for x in jazzmin_settings["hide_models"]] # Ensure icon model names and classes are lower case jazzmin_settings["icons"] = {x.lower(): y.lower() for x, y in jazzmin_settings.get("icons", {}).items()} # Default the site icon using the site logo jazzmin_settings["site_icon"] = jazzmin_settings["site_icon"] or jazzmin_settings["site_logo"] # Default the login logo using the site logo jazzmin_settings["login_logo"] = jazzmin_settings["login_logo"] or jazzmin_settings["site_logo"] # Default the login logo dark using the login logo jazzmin_settings["login_logo_dark"] = jazzmin_settings["login_logo_dark"] or jazzmin_settings["login_logo"] # ensure all model names are lower cased jazzmin_settings["changeform_format_overrides"] = { x.lower(): y.lower() for x, y in jazzmin_settings.get("changeform_format_overrides", {}).items() } return jazzmin_settings def get_ui_tweaks() -> Dict: raw_tweaks = copy.deepcopy(DEFAULT_UI_TWEAKS) raw_tweaks.update(getattr(settings, "JAZZMIN_UI_TWEAKS", {})) tweaks = {x: y for x, y in raw_tweaks.items() if y not in (None, "", False)} # These options dont work well together if tweaks.get("layout_boxed"): tweaks.pop("navbar_fixed", None) tweaks.pop("footer_fixed", None) bool_map = { "navbar_small_text": "text-sm", "footer_small_text": "text-sm", "body_small_text": "text-sm", "brand_small_text": "text-sm", "sidebar_nav_small_text": "text-sm", "no_navbar_border": "border-bottom-0", "sidebar_disable_expand": "sidebar-no-expand", "sidebar_nav_child_indent": "nav-child-indent", "sidebar_nav_compact_style": "nav-compact", "sidebar_nav_legacy_style": "nav-legacy", "sidebar_nav_flat_style": "nav-flat", "layout_boxed": "layout-boxed", "sidebar_fixed": "layout-fixed", "navbar_fixed": "layout-navbar-fixed", "footer_fixed": "layout-footer-fixed", "actions_sticky_top": "sticky-top", } for key, value in bool_map.items(): if key in tweaks: tweaks[key] = value def classes(*args: str) -> str: return " ".join([tweaks.get(arg, "") for arg in args]).strip() theme = tweaks["theme"] if theme not in THEMES: logger.warning("{} not found in {}, using default".format(theme, THEMES.keys())) theme = "default" dark_mode_theme = tweaks.get("dark_mode_theme", None) if dark_mode_theme and dark_mode_theme not in DARK_THEMES: logger.warning("{} is not a dark theme, using darkly".format(dark_mode_theme)) dark_mode_theme = "darkly" theme_body_classes = " theme-{}".format(theme) if theme in DARK_THEMES: theme_body_classes += " dark-mode" ret = { "raw": raw_tweaks, "theme": {"name": theme, "src": static(THEMES[theme])}, "sidebar_classes": classes("sidebar", "sidebar_disable_expand"), "navbar_classes": classes("navbar", "no_navbar_border", "navbar_small_text"), "body_classes": classes( "accent", "body_small_text", "navbar_fixed", "footer_fixed", "sidebar_fixed", "layout_boxed" ) + theme_body_classes, "actions_classes": classes("actions_sticky_top"), "sidebar_list_classes": classes( "sidebar_nav_small_text", "sidebar_nav_flat_style", "sidebar_nav_legacy_style", "sidebar_nav_child_indent", "sidebar_nav_compact_style", ), "brand_classes": classes("brand_small_text", "brand_colour"), "footer_classes": classes("footer_small_text"), "button_classes": tweaks["button_classes"], } if dark_mode_theme: ret["dark_mode_theme"] = {"name": dark_mode_theme, "src": static(THEMES[dark_mode_theme])} return ret