Coverage for src/backend/InvenTree/common/icons.py: 59%
44 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Icon utilities for InvenTree."""
3import json
4from dataclasses import dataclass
5from pathlib import Path
6from typing import TypedDict
8from django.core.exceptions import ValidationError
9from django.templatetags.static import static
11import structlog
13logger = structlog.get_logger('inventree')
15_icon_packs = None
18class Icon(TypedDict):
19 """Dict type for an icon.
21 Attributes:
22 name: The name of the icon.
23 category: The category of the icon.
24 tags: A list of tags for the icon (used for search).
25 variants: A dictionary of variants for the icon, where the key is the variant name and the value is the variant's unicode hex character.
26 """
28 name: str
29 category: str
30 tags: list[str]
31 variants: dict[str, str]
34@dataclass
35class IconPack:
36 """Dataclass for an icon pack.
38 Attributes:
39 name: The name of the icon pack.
40 prefix: The prefix used for the icon pack.
41 fonts: A dictionary of different font file formats for the icon pack, where the key is the css format and the value a path to the font file.
42 icons: A dictionary of icons in the icon pack, where the key is the icon name and the value is a dictionary of the icon's variants.
43 """
45 name: str
46 prefix: str
47 fonts: dict[str, str]
48 icons: dict[str, Icon]
51def get_icon_packs():
52 """Return a dictionary of available icon packs including their icons."""
53 global _icon_packs
55 if _icon_packs is None:
56 tabler_icons_path = Path(__file__).parent.parent.joinpath(
57 'InvenTree/static/tabler-icons/icons.json'
58 )
59 with open(tabler_icons_path, encoding='utf-8') as tabler_icons_file:
60 tabler_icons = json.load(tabler_icons_file)
62 icon_packs = [
63 IconPack(
64 name='Tabler Icons',
65 prefix='ti',
66 fonts={
67 'woff2': static('tabler-icons/tabler-icons.woff2'),
68 'woff': static('tabler-icons/tabler-icons.woff'),
69 'truetype': static('tabler-icons/tabler-icons.ttf'),
70 },
71 icons=tabler_icons,
72 )
73 ]
75 from InvenTree.exceptions import log_error
76 from plugin import PluginMixinEnum, registry
78 for plugin in registry.with_mixin(PluginMixinEnum.ICON_PACK, active=True): 78 ↛ 79line 78 didn't jump to line 79 because the loop on line 78 never started
79 try:
80 icon_packs.extend(plugin.icon_packs())
81 except Exception as e:
82 log_error('get_icon_packs', plugin=plugin.slug)
83 logger.warning('Error loading icon pack from plugin %s: %s', plugin, e)
85 _icon_packs = {pack.prefix: pack for pack in icon_packs}
87 return _icon_packs
90def reload_icon_packs():
91 """Reload the icon packs."""
92 global _icon_packs
93 _icon_packs = None
94 get_icon_packs()
97def validate_icon(icon: str):
98 """Validate an icon string in the format pack:name:variant."""
99 try:
100 pack, name, variant = icon.split(':')
101 except ValueError:
102 raise ValidationError(
103 f'Invalid icon format: {icon}, expected: pack:name:variant'
104 )
106 packs = get_icon_packs()
108 if pack not in packs:
109 raise ValidationError(f'Invalid icon pack: {pack}')
111 if name not in packs[pack].icons:
112 raise ValidationError(f'Invalid icon name: {name}')
114 if variant not in packs[pack].icons[name]['variants']:
115 raise ValidationError(f'Invalid icon variant: {variant}')
117 return packs[pack], packs[pack].icons[name], variant