From 6393d7bdfde935fb41ab79ba99ab9f0f62cae7fc Mon Sep 17 00:00:00 2001 From: Anders Einar Hilden Date: Tue, 16 Jul 2024 17:50:38 +0200 Subject: [PATCH] Add BS_ICONS_BASE_PATH and MD_ICONS_BASE_PATH to use local copies of icon sets. (#26) This removes the need to call out to CDNs to download the icons. --- README.md | 18 +++- .../templatetags/bootstrap_icons.py | 91 ++++++++++++------- 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index cec8f64..c58037d 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,13 @@ BS_ICONS_BASE_URL = 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/' BS_ICONS_BASE_URL defaults to the latest boostrap-icons CDN that was available when releasing this package. Change the URL to use an older or newer one. +You can also load icons from a local path. This will disable downloading icons +from BS_ICONS_BASE_URL: + +``` +BS_ICONS_BASE_PATH = 'node_modules/bootstrap-icons/' +``` + To add custom icons to your app you need to set the path where these can be found. The default setting is *custom-icons*, so you would add your icons to */your-app/static/custom-icons/*. @@ -167,7 +174,14 @@ Material Desing Icons are loaded from the default URL: MD_ICONS_BASE_URL = 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.2.96/' ``` -You can change it to your desired location by overriding this setting. +You can change it to your desired location by overriding this setting. + +You can also load icons from a local path. This will disable downloading icons +from MD_ICONS_BASE_URL: + +``` +MD_ICONS_BASE_PATH = 'node_modules/@mdi/svg' +``` ### Configure icon cache @@ -185,7 +199,7 @@ and stored to a local file. On each subsequent use the icon will be simply loade In case icons are not found you can configure, what to display: ``` -BS_ICONS_NOT_FOUND = f"Icon <{icon_path}> does not exist" +BS_ICONS_NOT_FOUND = f"Icon `{icon_path}` does not exist" ``` This shows the error message if you for example misspelled an icon name. diff --git a/django_bootstrap_icons/templatetags/bootstrap_icons.py b/django_bootstrap_icons/templatetags/bootstrap_icons.py index ad92466..5690403 100644 --- a/django_bootstrap_icons/templatetags/bootstrap_icons.py +++ b/django_bootstrap_icons/templatetags/bootstrap_icons.py @@ -1,5 +1,6 @@ """ django bootstrap icons templatetags """ import os +from pathlib import Path import requests from defusedxml.minidom import parse, parseString @@ -87,18 +88,19 @@ def custom_icon(icon_name, size=None, color=None, extra_classes=None): try: content = parse(icon_path) except FileNotFoundError: - return f"Icon <{icon_path}> does not exist" + return f"Icon `{icon_path}` does not exist" return mark_safe(render_svg(content, size, color, extra_classes)) def get_icon(icon_path, icon_name, size=None, color=None, extra_classes=None): """ Manage caching of bootstrap icons - :param str icon_path: icon path given by CDN + :param icon_path icon_path: icon path given by CDN or local path :param str icon_name: Name of custom icon to render :param str size: size of custom icon to render :param str color: color of custom icon to render :param str extra_classes: String of classes to add to icon + :type icon_path: str or Path """ cache_path = getattr( settings, @@ -119,26 +121,43 @@ def get_icon(icon_path, icon_name, size=None, color=None, extra_classes=None): # cached icon doesn't exist or no cache configured, create and return icon try: - resp = requests.get(icon_path, timeout=20) - if resp.status_code >= 400: - # return f"Icon <{icon_path}> does not exist" - return getattr( - settings, - 'BS_ICONS_NOT_FOUND', - f"Icon <{icon_path}> does not exist" - ) - content = parseString(resp.text) - svg = render_svg(content, size, color, extra_classes) - # if cache configured write icon to cache - if cache_path and cache_file: - with open(cache_file, 'w', encoding="utf-8") as icon_file: - icon_file.write(svg) - except requests.ConnectionError: + if isinstance(icon_path, Path): + # icon_path is local path + with icon_path.open("r") as icon_file: + svg_string = icon_file.read() + else: + # icon_path is URL + resp = requests.get(icon_path, timeout=20) + if resp.status_code == 404: + # co-opt FileNotFoundError for HTTP 404 + msg = "Got HTTP 404 when downloading icon" + raise FileNotFoundError(msg) + resp.raise_for_status() + svg_string = resp.text + + except FileNotFoundError: + # The icon was not found (on disk, or on the web) return getattr( settings, - 'BS_ICONS_NOT_FOUND', - f"Icon <{icon_path}> does not exist" + "BS_ICONS_NOT_FOUND", + f"Icon `{icon_path}` does not exist", + ) + + # RequestException includes ConnectionError, Timeout, HTTPError and TooManyRedirects + except (requests.exceptions.RequestException, OSError): + # We failed to get the icon, but it might exist + return getattr( + settings, "BS_ICONS_NOT_FOUND", f"Failed to read icon `{icon_path}`" ) + + content = parseString(svg_string) + svg = render_svg(content, size, color, extra_classes) + + # if cache configured write icon to cache + if cache_path and cache_file: + with open(cache_file, "w", encoding="utf-8") as icon_file: + icon_file.write(svg) + return svg @@ -154,12 +173,17 @@ def bs_icon(icon_name, size=None, color=None, extra_classes=None): if icon_name is None: return '' - base_url = getattr( - settings, - 'BS_ICONS_BASE_URL', - 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/', - ) - icon_path = f'{base_url}icons/{icon_name}.svg' + base_path = getattr(settings, "BS_ICONS_BASE_PATH", None) + + if base_path: + icon_path = Path(base_path, "icons", f"{icon_name}.svg") + else: + base_url = getattr( + settings, + "BS_ICONS_BASE_URL", + "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/", + ) + icon_path = f"{base_url}icons/{icon_name}.svg" svg = get_icon(icon_path, icon_name, size, color, extra_classes) return mark_safe(svg) @@ -187,12 +211,17 @@ def md_icon(icon_name, size=None, color=None, extra_classes=None): if size is None: size = '20' - base_url = getattr( - settings, - 'MD_ICONS_BASE_URL', - 'https://cdn.jsdelivr.net/npm/@mdi/svg@7.2.96/' - ) - icon_path = f'{base_url}svg/{icon_name}.svg' + base_path = getattr(settings, "MD_ICONS_BASE_PATH", None) + + if base_path: + icon_path = Path(base_path, "svg", f"{icon_name}.svg") + else: + base_url = getattr( + settings, + "MD_ICONS_BASE_URL", + "https://cdn.jsdelivr.net/npm/@mdi/svg@7.2.96/", + ) + icon_path = f"{base_url}svg/{icon_name}.svg" svg = get_icon(icon_path, icon_name, size, color, extra_classes) return mark_safe(svg)