From 25732285695a2f6a491c091bbd7ecb4126e222ad Mon Sep 17 00:00:00 2001 From: catborise Date: Thu, 5 Nov 2026 12:07:10 +0300 Subject: [PATCH] chore(deps): the icons come from the committed cache, not django-bootstrap-icons django-bootstrap-icons (last release July 2024) only read the SVG of each {% bs_icon %} from static/icon_cache; for an icon not there it fetched the SVG from a CDN while rendering the page, and it brought defusedxml along. A 20-line template tag in webvirtcloud reads the same files; the package, its CDN path and the BS_ICONS_CACHE setting go. requests stays: websockify needs it. --- conf/requirements.txt | 3 +-- webvirtcloud/bootstrap_icons.py | 24 ++++++++++++++++++++++++ webvirtcloud/settings.py.template | 4 +--- webvirtcloud/tests/test_icon_cache.py | 15 +++++++++++---- webvirtcloud/tests/test_static_files.py | 5 ++--- 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 webvirtcloud/bootstrap_icons.py diff --git a/conf/requirements.txt b/conf/requirements.txt index e8b51520..c0f4df70 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -1,8 +1,7 @@ Django==5.2.18 django_bootstrap5==26.3 -django-bootstrap-icons==0.9.0 django-otp==1.7.4 -# for django-bootstrap-icons and websockify; a host's python3-requests would +# for websockify; a host's python3-requests would # otherwise bring idna, requests and urllib3 with advisories idna>=3.15 requests>=2.33.0 diff --git a/webvirtcloud/bootstrap_icons.py b/webvirtcloud/bootstrap_icons.py new file mode 100644 index 00000000..603c8a15 --- /dev/null +++ b/webvirtcloud/bootstrap_icons.py @@ -0,0 +1,24 @@ +"""{% bs_icon %}: a Bootstrap icon's SVG, inline, from static/icon_cache, +which holds each icon a template uses at the size, color and classes it +asks for (webvirtcloud/tests/test_icon_cache.py checks every template).""" + +from functools import cache +from pathlib import Path + +from django import template +from django.conf import settings +from django.utils.safestring import mark_safe + +register = template.Library() + +ICONS = Path(settings.BASE_DIR, "static", "icon_cache") + + +@register.simple_tag +def bs_icon(name, size=None, color=None, extra_classes=None): + return _svg(f"{name}_{size}_{color}_{extra_classes}.svg".replace(" ", "_")) + + +@cache # one file a literal of the templates +def _svg(file_name): + return mark_safe((ICONS / file_name).read_text(encoding="utf-8")) diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index 0365fe0e..ff43b1d8 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -62,7 +62,6 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "django_bootstrap5", - "django_bootstrap_icons", "django_otp", "django_otp.plugins.otp_totp", "drf_spectacular", @@ -127,6 +126,7 @@ TEMPLATES = [ ], "libraries": { "common_tags": "webvirtcloud.common_tags", + "bootstrap_icons": "webvirtcloud.bootstrap_icons", }, }, }, @@ -227,8 +227,6 @@ STATICFILES_DIRS = [BASE_DIR / "static"] STATIC_ROOT = BASE_DIR / "staticfiles" -BS_ICONS_CACHE = Path.joinpath(BASE_DIR, 'static/icon_cache') - # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field diff --git a/webvirtcloud/tests/test_icon_cache.py b/webvirtcloud/tests/test_icon_cache.py index 5acbfbd1..06d2583d 100644 --- a/webvirtcloud/tests/test_icon_cache.py +++ b/webvirtcloud/tests/test_icon_cache.py @@ -1,12 +1,14 @@ """Every {% bs_icon %} of the templates is in the committed icon cache -(BS_ICONS_CACHE). A missing one is fetched from the CDN when a page is -rendered, or shown as an error where the server has no internet access.""" +(webvirtcloud/bootstrap_icons.py): a missing one is an error page.""" import re import unittest from pathlib import Path from django.conf import settings +from django.template import Context, Template + +from webvirtcloud.bootstrap_icons import ICONS BASE = Path(settings.BASE_DIR) CALL = re.compile(r"\{%\s*bs_icon\s+(.*?)\s*%\}") @@ -16,7 +18,7 @@ KWARG = re.compile(r"""(\w+)=['"]([^'"]*)['"]""") class IconCacheTestCase(unittest.TestCase): def test_every_template_icon_is_cached(self): - cache = Path(settings.BS_ICONS_CACHE) + cache = ICONS missing = [] templates = [*BASE.glob("templates/**/*.html"), *BASE.glob("*/templates/**/*.html")] self.assertGreater(len(templates), 50) # the globs still find the templates @@ -25,8 +27,13 @@ class IconCacheTestCase(unittest.TestCase): name = NAME.match(args) self.assertIsNotNone(name, f"{template}: icon name is not a literal: {args}") kw = dict(KWARG.findall(args)) - # the file name django-bootstrap-icons uses for its cache + # the file name bs_icon reads cached = f"{name.group(1)}_{kw.get('size')}_{kw.get('color')}_{kw.get('extra_classes')}.svg".replace(" ", "_") if not (cache / cached).exists(): missing.append(f"{template.relative_to(BASE)}: {cached}") self.assertEqual(missing, []) + + def test_an_icon_is_its_svg_inline(self): + html = Template("{% load bootstrap_icons %}{% bs_icon 'pencil' %}").render(Context()) + self.assertEqual(html, (ICONS / "pencil_None_None_None.svg").read_text(encoding="utf-8")) + self.assertIn("