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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Validation helpers for common models."""
3import re
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 _
10import common.icons
11from common.settings import get_global_setting
14def parameter_model_types():
15 """Return a list of valid parameter model choices."""
16 import InvenTree.models
18 return list(
19 InvenTree.helpers_model.getModelsWithMixin(
20 InvenTree.models.InvenTreeParameterMixin
21 )
22 )
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 ]
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 ]
40 return [(None, _('All models')), *options]
43def attachment_model_types():
44 """Return a list of valid attachment model choices."""
45 import InvenTree.models
47 return list(
48 InvenTree.helpers_model.getModelsWithMixin(
49 InvenTree.models.InvenTreeAttachmentMixin
50 )
51 )
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 ]
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'))
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
71 raise ValidationError(_('Invalid attachment model type') + f": '{label}'")
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')
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)
85 if attachment.size > (max_size * 1024 * 1024):
86 raise ValidationError(
87 _(f'File size exceeds maximum upload limit of {max_size} MB')
88 )
90 try:
91 default_storage.generate_filename(attachment.name)
92 except SuspiciousFileOperation: # pragma: no cover
93 raise ValidationError(_('Invalid file name'))
96def validate_notes_model_type(value):
97 """Ensure that the provided model type is valid.
99 The provided value must map to a model which implements the 'InvenTreeNotesMixin'.
100 """
101 import InvenTree.helpers_model
102 import InvenTree.models
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
108 model_types = list(
109 InvenTree.helpers_model.getModelsWithMixin(InvenTree.models.InvenTreeNotesMixin)
110 )
112 model_names = [model.__name__.lower() for model in model_types]
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}'")
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
126 if value > places_max:
127 raise ValidationError(_('Minimum places cannot be greater than maximum places'))
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
138 if value < places_min:
139 raise ValidationError(_('Maximum places cannot be less than minimum places'))
142def validate_email_domains(setting):
143 """Validate the email domains setting."""
144 if not setting.value:
145 return
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}'))
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
160 common.icons.validate_icon(name)
163def validate_uppercase(value: str):
164 """Ensure that the provided value is uppercase."""
165 value = str(value)
167 if value != value.upper():
168 raise ValidationError(_('Value must be uppercase'))
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'))
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
182 if not value:
183 return
185 try:
186 Locale.parse(translation.to_locale(value))
187 except (UnknownLocaleError, ValueError) as e:
188 raise ValidationError(f"Invalid locale value: '{value}' - {e}")