settings.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import copy
  2. import logging
  3. from typing import Dict, Any
  4. from django.conf import settings
  5. from django.templatetags.static import static
  6. from .utils import get_admin_url, get_model_meta
  7. logger = logging.getLogger(__name__)
  8. DEFAULT_SETTINGS: Dict[str, Any] = {
  9. # title of the window (Will default to current_admin_site.site_title)
  10. "site_title": None,
  11. # Title on the login screen (19 chars max) (will default to current_admin_site.site_header)
  12. "site_header": None,
  13. # Title on the brand (19 chars max) (will default to current_admin_site.site_header)
  14. "site_brand": None,
  15. # Relative path to logo for your site, used for brand on top left (must be present in static files)
  16. "site_logo": "vendor/adminlte/img/AdminLTELogo.png",
  17. # Relative path to logo for your site, used for login logo (must be present in static files. Defaults to site_logo)
  18. "login_logo": None,
  19. # Logo to use for login form in dark themes (must be present in static files. Defaults to login_logo)
  20. "login_logo_dark": None,
  21. # CSS classes that are applied to the logo
  22. "site_logo_classes": "img-circle",
  23. # Relative path to a favicon for your site, will default to site_logo if absent (ideally 32x32 px)
  24. "site_icon": None,
  25. # Welcome text on the login screen
  26. "welcome_sign": "Welcome",
  27. # Copyright on the footer
  28. "copyright": "",
  29. # The model admin to search from the search bar, search bar omitted if excluded
  30. "search_model": None,
  31. # Field name on user model that contains avatar ImageField/URLField/Charfield or a callable that receives the user
  32. "user_avatar": None,
  33. ############
  34. # Top Menu #
  35. ############
  36. # Links to put along the nav bar
  37. "topmenu_links": [],
  38. #############
  39. # User Menu #
  40. #############
  41. # Additional links to include in the user menu on the top right ('app' url type is not allowed)
  42. "usermenu_links": [],
  43. #############
  44. # Side Menu #
  45. #############
  46. # Whether to display the side menu
  47. "show_sidebar": True,
  48. # Whether to aut expand the menu
  49. "navigation_expanded": True,
  50. # Hide these apps when generating side menu e.g (auth)
  51. "hide_apps": [],
  52. # Hide these models when generating side menu (e.g auth.user)
  53. "hide_models": [],
  54. # List of apps to base side menu ordering off of
  55. "order_with_respect_to": [],
  56. # Custom links to append to side menu app groups, keyed on app name
  57. "custom_links": {},
  58. # Custom icons for side menu apps/models See the link below
  59. # 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,
  60. # 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
  61. # for the full list of 5.13.0 free icon classes
  62. "icons": {"auth": "fas fa-users-cog", "auth.user": "fas fa-user", "auth.Group": "fas fa-users"},
  63. # Icons that are used when one is not manually specified
  64. "default_icon_parents": "fas fa-chevron-circle-right",
  65. "default_icon_children": "fas fa-circle",
  66. #################
  67. # Related Modal #
  68. #################
  69. # Activate Bootstrap modal
  70. "related_modal_active": False,
  71. #############
  72. # UI Tweaks #
  73. #############
  74. # Relative paths to custom CSS/JS scripts (must be present in static files)
  75. "custom_css": None,
  76. "custom_js": None,
  77. # Whether to link font from fonts.googleapis.com (use custom_css to supply font otherwise)
  78. "use_google_fonts_cdn": True,
  79. # Whether to show the UI customizer on the sidebar
  80. "show_ui_builder": False,
  81. ###############
  82. # Change view #
  83. ###############
  84. # Render out the change view as a single form, or in tabs, current options are
  85. # - single
  86. # - horizontal_tabs (default)
  87. # - vertical_tabs
  88. # - collapsible
  89. # - carousel
  90. "changeform_format": "horizontal_tabs",
  91. # override change forms on a per modeladmin basis
  92. "changeform_format_overrides": {},
  93. # Add a language dropdown into the admin
  94. "language_chooser": False,
  95. }
  96. #######################################
  97. # Currently available UI tweaks #
  98. # Use the UI builder to generate this #
  99. #######################################
  100. DEFAULT_UI_TWEAKS: Dict[str, Any] = {
  101. # Small text on the top navbar
  102. "navbar_small_text": False,
  103. # Small text on the footer
  104. "footer_small_text": False,
  105. # Small text everywhere
  106. "body_small_text": False,
  107. # Small text on the brand/logo
  108. "brand_small_text": False,
  109. # brand/logo background colour
  110. "brand_colour": False,
  111. # Link colour
  112. "accent": "accent-primary",
  113. # topmenu colour
  114. "navbar": "navbar-white navbar-light",
  115. # topmenu border
  116. "no_navbar_border": False,
  117. # Make the top navbar sticky, keeping it in view as you scroll
  118. "navbar_fixed": False,
  119. # Whether to constrain the page to a box (leaving big margins at the side)
  120. "layout_boxed": False,
  121. # Make the footer sticky, keeping it in view all the time
  122. "footer_fixed": False,
  123. # Make the sidebar sticky, keeping it in view as you scroll
  124. "sidebar_fixed": False,
  125. # sidemenu colour
  126. "sidebar": "sidebar-dark-primary",
  127. # sidemenu small text
  128. "sidebar_nav_small_text": False,
  129. # Disable expanding on hover of collapsed sidebar
  130. "sidebar_disable_expand": False,
  131. # Indent child menu items on sidebar
  132. "sidebar_nav_child_indent": False,
  133. # Use a compact sidebar
  134. "sidebar_nav_compact_style": False,
  135. # Use the AdminLTE2 style sidebar
  136. "sidebar_nav_legacy_style": False,
  137. # Use a flat style sidebar
  138. "sidebar_nav_flat_style": False,
  139. # Bootstrap theme to use (default, or from bootswatch, see THEMES below)
  140. "theme": "default",
  141. # Theme to use instead if the user has opted for dark mode (e.g darkly/cyborg/slate/solar/superhero)
  142. "dark_mode_theme": None,
  143. # The classes/styles to use with buttons
  144. "button_classes": {
  145. "primary": "btn-primary",
  146. "secondary": "btn-secondary",
  147. "info": "btn-info",
  148. "warning": "btn-warning",
  149. "danger": "btn-danger",
  150. "success": "btn-success",
  151. },
  152. }
  153. THEMES = {
  154. # light themes
  155. "default": "vendor/bootswatch/default/bootstrap.min.css",
  156. "cerulean": "vendor/bootswatch/cerulean/bootstrap.min.css",
  157. "cosmo": "vendor/bootswatch/cosmo/bootstrap.min.css",
  158. "flatly": "vendor/bootswatch/flatly/bootstrap.min.css",
  159. "journal": "vendor/bootswatch/journal/bootstrap.min.css",
  160. "litera": "vendor/bootswatch/litera/bootstrap.min.css",
  161. "lumen": "vendor/bootswatch/lumen/bootstrap.min.css",
  162. "lux": "vendor/bootswatch/lux/bootstrap.min.css",
  163. "materia": "vendor/bootswatch/materia/bootstrap.min.css",
  164. "minty": "vendor/bootswatch/minty/bootstrap.min.css",
  165. "pulse": "vendor/bootswatch/pulse/bootstrap.min.css",
  166. "sandstone": "vendor/bootswatch/sandstone/bootstrap.min.css",
  167. "simplex": "vendor/bootswatch/simplex/bootstrap.min.css",
  168. "sketchy": "vendor/bootswatch/sketchy/bootstrap.min.css",
  169. "spacelab": "vendor/bootswatch/spacelab/bootstrap.min.css",
  170. "united": "vendor/bootswatch/united/bootstrap.min.css",
  171. "yeti": "vendor/bootswatch/yeti/bootstrap.min.css",
  172. # dark themes
  173. "darkly": "vendor/bootswatch/darkly/bootstrap.min.css",
  174. "cyborg": "vendor/bootswatch/cyborg/bootstrap.min.css",
  175. "slate": "vendor/bootswatch/slate/bootstrap.min.css",
  176. "solar": "vendor/bootswatch/solar/bootstrap.min.css",
  177. "superhero": "vendor/bootswatch/superhero/bootstrap.min.css",
  178. }
  179. DARK_THEMES = ("darkly", "cyborg", "slate", "solar", "superhero")
  180. CHANGEFORM_TEMPLATES = {
  181. "single": "jazzmin/includes/single.html",
  182. "carousel": "jazzmin/includes/carousel.html",
  183. "collapsible": "jazzmin/includes/collapsible.html",
  184. "horizontal_tabs": "jazzmin/includes/horizontal_tabs.html",
  185. "vertical_tabs": "jazzmin/includes/vertical_tabs.html",
  186. }
  187. def get_search_model_string(search_model: str) -> str:
  188. """
  189. Get a search model string for reversing an admin url.
  190. Ensure the model name is lower cased but remain the app name untouched.
  191. """
  192. app, model_name = search_model.split(".")
  193. return "{app}.{model_name}".format(app=app, model_name=model_name.lower())
  194. def get_settings() -> Dict:
  195. jazzmin_settings = copy.deepcopy(DEFAULT_SETTINGS)
  196. user_settings = {x: y for x, y in getattr(settings, "JAZZMIN_SETTINGS", {}).items() if y is not None}
  197. jazzmin_settings.update(user_settings)
  198. # Extract search model configuration from search_model setting
  199. if jazzmin_settings["search_model"]:
  200. if not isinstance(jazzmin_settings["search_model"], list):
  201. jazzmin_settings["search_model"] = [jazzmin_settings["search_model"]]
  202. jazzmin_settings["search_models_parsed"] = []
  203. for search_model in jazzmin_settings["search_model"]:
  204. jazzmin_search_model = {}
  205. jazzmin_search_model["search_url"] = get_admin_url(get_search_model_string(search_model))
  206. model_meta = get_model_meta(search_model)
  207. if model_meta:
  208. jazzmin_search_model["search_name"] = model_meta.verbose_name_plural.title()
  209. else:
  210. jazzmin_search_model["search_name"] = search_model.split(".")[-1] + "s"
  211. jazzmin_settings["search_models_parsed"].append(jazzmin_search_model)
  212. # Deal with single strings in hide_apps/hide_models and make sure we lower case 'em
  213. if type(jazzmin_settings["hide_apps"]) == str:
  214. jazzmin_settings["hide_apps"] = [jazzmin_settings["hide_apps"]]
  215. jazzmin_settings["hide_apps"] = [x.lower() for x in jazzmin_settings["hide_apps"]]
  216. if type(jazzmin_settings["hide_models"]) == str:
  217. jazzmin_settings["hide_models"] = [jazzmin_settings["hide_models"]]
  218. jazzmin_settings["hide_models"] = [x.lower() for x in jazzmin_settings["hide_models"]]
  219. # Ensure icon model names and classes are lower case
  220. jazzmin_settings["icons"] = {x.lower(): y.lower() for x, y in jazzmin_settings.get("icons", {}).items()}
  221. # Default the site icon using the site logo
  222. jazzmin_settings["site_icon"] = jazzmin_settings["site_icon"] or jazzmin_settings["site_logo"]
  223. # Default the login logo using the site logo
  224. jazzmin_settings["login_logo"] = jazzmin_settings["login_logo"] or jazzmin_settings["site_logo"]
  225. # Default the login logo dark using the login logo
  226. jazzmin_settings["login_logo_dark"] = jazzmin_settings["login_logo_dark"] or jazzmin_settings["login_logo"]
  227. # ensure all model names are lower cased
  228. jazzmin_settings["changeform_format_overrides"] = {
  229. x.lower(): y.lower() for x, y in jazzmin_settings.get("changeform_format_overrides", {}).items()
  230. }
  231. return jazzmin_settings
  232. def get_ui_tweaks() -> Dict:
  233. raw_tweaks = copy.deepcopy(DEFAULT_UI_TWEAKS)
  234. raw_tweaks.update(getattr(settings, "JAZZMIN_UI_TWEAKS", {}))
  235. tweaks = {x: y for x, y in raw_tweaks.items() if y not in (None, "", False)}
  236. # These options dont work well together
  237. if tweaks.get("layout_boxed"):
  238. tweaks.pop("navbar_fixed", None)
  239. tweaks.pop("footer_fixed", None)
  240. bool_map = {
  241. "navbar_small_text": "text-sm",
  242. "footer_small_text": "text-sm",
  243. "body_small_text": "text-sm",
  244. "brand_small_text": "text-sm",
  245. "sidebar_nav_small_text": "text-sm",
  246. "no_navbar_border": "border-bottom-0",
  247. "sidebar_disable_expand": "sidebar-no-expand",
  248. "sidebar_nav_child_indent": "nav-child-indent",
  249. "sidebar_nav_compact_style": "nav-compact",
  250. "sidebar_nav_legacy_style": "nav-legacy",
  251. "sidebar_nav_flat_style": "nav-flat",
  252. "layout_boxed": "layout-boxed",
  253. "sidebar_fixed": "layout-fixed",
  254. "navbar_fixed": "layout-navbar-fixed",
  255. "footer_fixed": "layout-footer-fixed",
  256. "actions_sticky_top": "sticky-top",
  257. }
  258. for key, value in bool_map.items():
  259. if key in tweaks:
  260. tweaks[key] = value
  261. def classes(*args: str) -> str:
  262. return " ".join([tweaks.get(arg, "") for arg in args]).strip()
  263. theme = tweaks["theme"]
  264. if theme not in THEMES:
  265. logger.warning("{} not found in {}, using default".format(theme, THEMES.keys()))
  266. theme = "default"
  267. dark_mode_theme = tweaks.get("dark_mode_theme", None)
  268. if dark_mode_theme and dark_mode_theme not in DARK_THEMES:
  269. logger.warning("{} is not a dark theme, using darkly".format(dark_mode_theme))
  270. dark_mode_theme = "darkly"
  271. theme_body_classes = " theme-{}".format(theme)
  272. if theme in DARK_THEMES:
  273. theme_body_classes += " dark-mode"
  274. ret = {
  275. "raw": raw_tweaks,
  276. "theme": {"name": theme, "src": static(THEMES[theme])},
  277. "sidebar_classes": classes("sidebar", "sidebar_disable_expand"),
  278. "navbar_classes": classes("navbar", "no_navbar_border", "navbar_small_text"),
  279. "body_classes": classes(
  280. "accent", "body_small_text", "navbar_fixed", "footer_fixed", "sidebar_fixed", "layout_boxed"
  281. )
  282. + theme_body_classes,
  283. "actions_classes": classes("actions_sticky_top"),
  284. "sidebar_list_classes": classes(
  285. "sidebar_nav_small_text",
  286. "sidebar_nav_flat_style",
  287. "sidebar_nav_legacy_style",
  288. "sidebar_nav_child_indent",
  289. "sidebar_nav_compact_style",
  290. ),
  291. "brand_classes": classes("brand_small_text", "brand_colour"),
  292. "footer_classes": classes("footer_small_text"),
  293. "button_classes": tweaks["button_classes"],
  294. }
  295. if dark_mode_theme:
  296. ret["dark_mode_theme"] = {"name": dark_mode_theme, "src": static(THEMES[dark_mode_theme])}
  297. return ret