Coverage for src/backend/InvenTree/common/validators.py: 45%

93 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""Validation helpers for common models.""" 

2 

3import re 

4 

5from django.core.exceptions import SuspiciousFileOperation, ValidationError 

6from django.core.files.storage import default_storage 

7from django.utils import translation 

8from django.utils.translation import gettext_lazy as _ 

9 

10import common.icons 

11from common.settings import get_global_setting 

12 

13 

14def parameter_model_types(): 

15 """Return a list of valid parameter model choices.""" 

16 import InvenTree.models 

17 

18 return list( 

19 InvenTree.helpers_model.getModelsWithMixin( 

20 InvenTree.models.InvenTreeParameterMixin 

21 ) 

22 ) 

23 

24 

25def parameter_model_options(): 

26 """Return a list of options for models which support parameters.""" 

27 return [ 

28 (model.__name__.lower(), model._meta.verbose_name) 

29 for model in parameter_model_types() 

30 ] 

31 

32 

33def parameter_template_model_options(): 

34 """Return a list of options for models which support parameter templates.""" 

35 options = [ 

36 (model.__name__.lower(), model._meta.verbose_name) 

37 for model in parameter_model_types() 

38 ] 

39 

40 return [(None, _('All models')), *options] 

41 

42 

43def attachment_model_types(): 

44 """Return a list of valid attachment model choices.""" 

45 import InvenTree.models 

46 

47 return list( 

48 InvenTree.helpers_model.getModelsWithMixin( 

49 InvenTree.models.InvenTreeAttachmentMixin 

50 ) 

51 ) 

52 

53 

54def attachment_model_options(): 

55 """Return a list of options for models which support attachments.""" 

56 return [ 

57 (model.__name__.lower(), model._meta.verbose_name) 

58 for model in attachment_model_types() 

59 ] 

60 

61 

62def attachment_model_class_from_label(label: str): 

63 """Return the model class for the given label.""" 

64 if not label: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 raise ValidationError(_('No attachment model type provided')) 

66 

67 for model in attachment_model_types(): 67 ↛ 71line 67 didn't jump to line 71 because the loop on line 67 didn't complete

68 if model.__name__.lower() == label.lower(): 

69 return model 

70 

71 raise ValidationError(_('Invalid attachment model type') + f": '{label}'") 

72 

73 

74def validate_attachment_model_type(value): 

75 """Ensure that the provided attachment model is valid.""" 

76 model_names = [el[0] for el in attachment_model_options()] 

77 if value not in model_names: 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true

78 raise ValidationError('Model type does not support attachments') 

79 

80 

81def validate_attachment_file(attachment): 

82 """Ensure that the provided attachment file is valid.""" 

83 max_size = get_global_setting('INVENTREE_UPLOAD_MAX_SIZE', create=False) 

84 

85 if attachment.size > (max_size * 1024 * 1024): 

86 raise ValidationError( 

87 _(f'File size exceeds maximum upload limit of {max_size} MB') 

88 ) 

89 

90 try: 

91 default_storage.generate_filename(attachment.name) 

92 except SuspiciousFileOperation: # pragma: no cover 

93 raise ValidationError(_('Invalid file name')) 

94 

95 

96def validate_notes_model_type(value): 

97 """Ensure that the provided model type is valid. 

98 

99 The provided value must map to a model which implements the 'InvenTreeNotesMixin'. 

100 """ 

101 import InvenTree.helpers_model 

102 import InvenTree.models 

103 

104 if not value: 104 ↛ 106line 104 didn't jump to line 106 because the condition on line 104 was never true

105 # Empty values are allowed 

106 return 

107 

108 model_types = list( 

109 InvenTree.helpers_model.getModelsWithMixin(InvenTree.models.InvenTreeNotesMixin) 

110 ) 

111 

112 model_names = [model.__name__.lower() for model in model_types] 

113 

114 if value.lower() not in model_names: 114 ↛ exitline 114 didn't return from function 'validate_notes_model_type' because the condition on line 114 was always true

115 raise ValidationError(f"Invalid model type '{value}'") 

116 

117 

118def validate_decimal_places_min(value): 

119 """Validator for PRICING_DECIMAL_PLACES_MIN setting.""" 

120 try: 

121 value = int(value) 

122 places_max = int(get_global_setting('PRICING_DECIMAL_PLACES', create=False)) 

123 except Exception: 

124 return 

125 

126 if value > places_max: 

127 raise ValidationError(_('Minimum places cannot be greater than maximum places')) 

128 

129 

130def validate_decimal_places_max(value): 

131 """Validator for PRICING_DECIMAL_PLACES_MAX setting.""" 

132 try: 

133 value = int(value) 

134 places_min = int(get_global_setting('PRICING_DECIMAL_PLACES_MIN', create=False)) 

135 except Exception: 

136 return 

137 

138 if value < places_min: 

139 raise ValidationError(_('Maximum places cannot be less than minimum places')) 

140 

141 

142def validate_email_domains(setting): 

143 """Validate the email domains setting.""" 

144 if not setting.value: 

145 return 

146 

147 domains = setting.value.split(',') 

148 for domain in domains: 

149 if not domain: 

150 raise ValidationError(_('An empty domain is not allowed.')) 

151 if not re.match(r'^@[a-zA-Z0-9\.\-_]+$', domain): 

152 raise ValidationError(_(f'Invalid domain name: {domain}')) 

153 

154 

155def validate_icon(name: str | None): 

156 """Validate the provided icon name, and ignore if empty.""" 

157 if name == '' or name is None: 

158 return 

159 

160 common.icons.validate_icon(name) 

161 

162 

163def validate_uppercase(value: str): 

164 """Ensure that the provided value is uppercase.""" 

165 value = str(value) 

166 

167 if value != value.upper(): 

168 raise ValidationError(_('Value must be uppercase')) 

169 

170 

171def validate_variable_string(value: str): 

172 """The passed value must be a valid variable identifier string.""" 

173 if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value): 173 ↛ exitline 173 didn't return from function 'validate_variable_string' because the condition on line 173 was always true

174 raise ValidationError(_('Value must be a valid variable identifier')) 

175 

176 

177def validate_locale(value: str): 

178 """Validate that the provided value is a valid locale string.""" 

179 from babel import Locale 

180 from babel.core import UnknownLocaleError 

181 

182 if not value: 

183 return 

184 

185 try: 

186 Locale.parse(translation.to_locale(value)) 

187 except (UnknownLocaleError, ValueError) as e: 

188 raise ValidationError(f"Invalid locale value: '{value}' - {e}")