diff --git a/README.md b/README.md index ca855927..d4c92a54 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Did you decide to start using Unfold but you don't have time to make the switch - **Environment label**: distinguish between environments by displaying a label - **Nonrelated inlines**: displays nonrelated model as inline in changeform - **Parallel admin**: support for default admin in parallel with Unfold. [Admin migration guide](https://unfoldadmin.com/blog/migrating-django-admin-unfold/?utm_medium=github&utm_source=unfold) +- **Favicons**: built-in support for configuring various site favicons - **VS Code**: project configuration and development container is included ## Table of contents @@ -186,6 +187,14 @@ UNFOLD = { "dark": lambda request: static("logo-dark.svg"), # dark mode }, "SITE_SYMBOL": "speed", # symbol from icon set + "SITE_FAVICONS": [ + { + "rel": "icon", + "sizes": "32x32", + "type": "image/svg+xml", + "href": lambda request: static("favicon.svg"), + }, + ], "SHOW_HISTORY": True, # show/hide "History" button, default: True "SHOW_VIEW_ON_SITE": True, # show/hide "View on site" button, default: True "ENVIRONMENT": "sample_app.environment_callback", diff --git a/src/unfold/dataclasses.py b/src/unfold/dataclasses.py index 663f1c67..5c300921 100644 --- a/src/unfold/dataclasses.py +++ b/src/unfold/dataclasses.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Dict, Optional, Union +from typing import Callable, Dict, Optional, Union from .typing import ActionFunction @@ -12,3 +12,11 @@ class UnfoldAction: path: str attrs: Optional[Dict] = None object_id: Optional[Union[int, str]] = None + + +@dataclass +class Favicon: + href: Union[str, Callable] + rel: Optional[str] = None + type: Optional[str] = None + sizes: Optional[str] = None diff --git a/src/unfold/settings.py b/src/unfold/settings.py index 5da9f979..5a31a11b 100644 --- a/src/unfold/settings.py +++ b/src/unfold/settings.py @@ -7,6 +7,7 @@ "SITE_ICON": None, "SITE_SYMBOL": None, "SITE_LOGO": None, + "SITE_FAVICONS": [], "SHOW_HISTORY": True, "SHOW_VIEW_ON_SITE": True, "COLORS": { diff --git a/src/unfold/sites.py b/src/unfold/sites.py index 4fe5d330..630f76ee 100644 --- a/src/unfold/sites.py +++ b/src/unfold/sites.py @@ -10,6 +10,7 @@ from django.utils.functional import lazy from django.utils.module_loading import import_string +from .dataclasses import Favicon from .settings import get_config from .utils import hex_to_rgb from .widgets import CHECKBOX_CLASSES, INPUT_CLASSES @@ -66,6 +67,9 @@ def each_context(self, request: HttpRequest) -> Dict[str, Any]: "site_symbol": self._get_value( get_config(self.settings_name)["SITE_SYMBOL"], request ), + "site_favicons": self._process_favicons( + request, get_config(self.settings_name)["SITE_FAVICONS"] + ), "show_history": get_config(self.settings_name)["SHOW_HISTORY"], "show_view_on_site": get_config(self.settings_name)[ "SHOW_VIEW_ON_SITE" @@ -350,6 +354,19 @@ def _replace_values(self, target: Dict, source: Dict, request: HttpRequest): return target + def _process_favicons( + self, request: HttpRequest, favicons: List[Dict] + ) -> List[Favicon]: + return [ + Favicon( + href=self._get_value(item["href"], request), + rel=item.get("rel"), + sizes=item.get("sizes"), + type=item.get("type"), + ) + for item in favicons + ] + def _process_colors( self, colors: Dict[str, Dict[str, str]] ) -> Dict[str, Dict[str, str]]: diff --git a/src/unfold/templates/unfold/layouts/skeleton.html b/src/unfold/templates/unfold/layouts/skeleton.html index 29f33749..a8ebb907 100644 --- a/src/unfold/templates/unfold/layouts/skeleton.html +++ b/src/unfold/templates/unfold/layouts/skeleton.html @@ -24,6 +24,10 @@ {% endfor %} + {% for favicon in site_favicons %} + + {% endfor %} + diff --git a/tests/test_site_branding.py b/tests/test_site_branding.py index 4a64fe62..4529e96d 100644 --- a/tests/test_site_branding.py +++ b/tests/test_site_branding.py @@ -143,3 +143,29 @@ def test_incorrect_mode_site_logo(self): request.user = AnonymousUser() context = admin_site.each_context(request) self.assertIsNone(context["site_logo"]) + + @override_settings( + UNFOLD={ + **CONFIG_DEFAULTS, + **{ + "SITE_FAVICONS": [ + { + "rel": "icon", + "sizes": "32x32", + "type": "image/svg+xml", + "href": lambda request: static("favicon.svg"), + } + ] + }, + } + ) + def test_favicons(self): + admin_site = UnfoldAdminSite() + request = RequestFactory().get("/rand") + request.user = AnonymousUser() + context = admin_site.each_context(request) + + self.assertEqual(context["site_favicons"][0].rel, "icon") + self.assertEqual(context["site_favicons"][0].sizes, "32x32") + self.assertEqual(context["site_favicons"][0].type, "image/svg+xml") + self.assertEqual(context["site_favicons"][0].href, "/static/favicon.svg")