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

1"""Icon utilities for InvenTree.""" 

2 

3import json 

4from dataclasses import dataclass 

5from pathlib import Path 

6from typing import TypedDict 

7 

8from django.core.exceptions import ValidationError 

9from django.templatetags.static import static 

10 

11import structlog 

12 

13logger = structlog.get_logger('inventree') 

14 

15_icon_packs = None 

16 

17 

18class Icon(TypedDict): 

19 """Dict type for an icon. 

20 

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 """ 

27 

28 name: str 

29 category: str 

30 tags: list[str] 

31 variants: dict[str, str] 

32 

33 

34@dataclass 

35class IconPack: 

36 """Dataclass for an icon pack. 

37 

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 """ 

44 

45 name: str 

46 prefix: str 

47 fonts: dict[str, str] 

48 icons: dict[str, Icon] 

49 

50 

51def get_icon_packs(): 

52 """Return a dictionary of available icon packs including their icons.""" 

53 global _icon_packs 

54 

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) 

61 

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 ] 

74 

75 from InvenTree.exceptions import log_error 

76 from plugin import PluginMixinEnum, registry 

77 

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) 

84 

85 _icon_packs = {pack.prefix: pack for pack in icon_packs} 

86 

87 return _icon_packs 

88 

89 

90def reload_icon_packs(): 

91 """Reload the icon packs.""" 

92 global _icon_packs 

93 _icon_packs = None 

94 get_icon_packs() 

95 

96 

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 ) 

105 

106 packs = get_icon_packs() 

107 

108 if pack not in packs: 

109 raise ValidationError(f'Invalid icon pack: {pack}') 

110 

111 if name not in packs[pack].icons: 

112 raise ValidationError(f'Invalid icon name: {name}') 

113 

114 if variant not in packs[pack].icons[name]['variants']: 

115 raise ValidationError(f'Invalid icon variant: {variant}') 

116 

117 return packs[pack], packs[pack].icons[name], variant