Coverage for src/accounts/validators/models.py: 82%
95 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-08-04 12:59 +0300
« prev ^ index » next coverage.py v7.9.2, created at 2025-08-04 12:59 +0300
1"""
2This module contains custom Django validators for model field validation.
4The validators use the @deconstructible decorator to make them serializable
5for Django migrations and form handling.
7Validators included:
8- OnlyDigitsValidator: Ensures field contains only numeric characters
9- NameValidator: Validates proper name format (letters, hyphens, apostrophes, spaces)
10- UsernameValidator: Validates username format (letters, numbers, underscores, 3-30 chars)
11- EmailOrUsernameValidator: Validates either email or username format
12"""
14from django.core.exceptions import ValidationError
15from django.core.validators import EmailValidator as DjangoEmailValidator
16from django.utils.deconstruct import deconstructible
17from django.utils.translation import gettext as _
19import re
22@deconstructible
23class OnlyDigitsValidator:
24 """
25 Validator that ensures a field contains only numeric characters.
27 This validator is used for fields like phone numbers where only
28 digits are allowed.
29 """
31 # Default error message for this validator
32 DEFAULT_MESSAGE = 'This field can contain only digits.'
34 def __init__(self, message=None):
35 """
36 Initialize the validator with an optional custom message.
37 """
38 self.message = message
40 @property
41 def message(self):
42 """
43 Get the current error message.
44 """
45 return self.__message
47 @message.setter
48 def message(self, value=None):
49 """
50 Set the error message, using default if None is provided.
51 """
52 if value is None:
53 self.__message = self.DEFAULT_MESSAGE
54 else:
55 self.__message = value
57 def __call__(self, value):
58 """
59 Validate that the value contains only digits.
61 This method is called by Django's validation system when
62 the field is validated. It checks if the string contains
63 only numeric characters.
64 """
65 if not value.isdigit():
66 raise ValidationError(self.__message)
69@deconstructible
70class NameValidator:
71 """
72 Validator that ensures a field contains only valid name characters.
74 This validator is used for name fields (first name, last name, city, country)
75 and allows letters, hyphens, apostrophes, and spaces.
76 """
78 # Default error message for this validator
79 DEFAULT_MESSAGE = (
80 'This field can contain only letters, hyphens, apostrophes and spaces.'
81 )
83 def __init__(self, message=None):
84 """
85 Initialize the validator with an optional custom message.
86 """
87 self.message = message
89 @property
90 def message(self):
91 """
92 Get the current error message.
93 """
94 return self.__message
96 @message.setter
97 def message(self, value=None):
98 """
99 Set the error message, using default if None is provided.
100 """
101 if value is None:
102 self.__message = self.DEFAULT_MESSAGE
103 else:
104 self.__message = value
106 def __call__(self, value):
107 """
108 Validate that the value contains only valid name characters.
110 This method normalizes different types of apostrophes and then
111 checks each character to ensure it's a letter, hyphen, apostrophe,
112 or space.
113 """
114 normalized = value.replace("'", "'")
116 for char in normalized:
117 if not (char.isalpha() or char in "'-" or char.isspace()):
118 raise ValidationError(self.__message)
121@deconstructible
122class UsernameValidator:
123 """
124 Validator that ensures a username follows proper format rules.
126 This validator ensures usernames are 3-30 characters long and contain
127 only letters, numbers, and underscores.
128 """
130 # Default error message for this validator
131 DEFAULT_MESSAGE = 'Username must be 3-30 characters long and contain only letters, numbers, and underscores.'
133 def __init__(self, message=None):
134 """
135 Initialize the validator with an optional custom message.
137 Args:
138 message: Custom error message to use instead of default
139 """
140 self.message = message
142 @property
143 def message(self):
144 """
145 Get the current error message.
147 Returns:
148 str: The error message to display on validation failure
149 """
150 return self.__message
152 @message.setter
153 def message(self, value=None):
154 """
155 Set the error message, using default if None is provided.
156 """
157 if value is None:
158 self.__message = self.DEFAULT_MESSAGE
159 else:
160 self.__message = value
162 def __call__(self, value):
163 """
164 Validate that the value follows username format rules.
166 This method uses a regular expression to validate the username format:
167 - 3-30 characters long
168 - Only letters (a-z, A-Z), numbers (0-9), and underscores (_)
170 Args:
171 value: The string value to validate
173 Raises:
174 ValidationError: If the value doesn't match username format
175 """
176 if not re.match(r'^[A-Za-z0-9_]{3,30}$', value):
177 raise ValidationError(self.__message)
180@deconstructible
181class EmailOrUsernameValidator:
182 """
183 Validator that accepts either a valid email address or username.
185 This validator is used for login fields where users can enter either
186 their email address or username. It first tries to validate as an email,
187 then as a username if email validation fails.
188 """
190 # Default error message for this validator
191 DEFAULT_MESSAGE = 'Please enter a valid email address or username.'
193 def __init__(self, message=None):
194 """
195 Initialize the validator with an optional custom message.
196 """
197 self.message = message
199 @property
200 def message(self):
201 """
202 Get the current error message.
203 """
204 return self.__message
206 @message.setter
207 def message(self, value=None):
208 """
209 Set the error message, using default if None is provided.
210 """
211 if value is None:
212 self.__message = self.DEFAULT_MESSAGE
213 else:
214 self.__message = value
216 def __call__(self, value):
217 """
218 Validate that the value is either a valid email or username.
220 This method first tries to validate the value as an email address
221 using Django's built-in EmailValidator. If that fails, it tries
222 to validate as a username using the same regex pattern as UsernameValidator.
223 """
224 django_email_validator = DjangoEmailValidator()
226 is_email = False
228 try:
229 django_email_validator(value)
230 is_email = True
231 except ValidationError:
232 pass
234 is_username = re.match(r'^[A-Za-z0-9_]{3,30}$', value)
236 if not (is_email or is_username):
237 raise ValidationError(self.__message)
240@deconstructible
241class FileSizeValidator:
242 def __init__(self, file_size_limit, message=None):
243 self.file_size_limit = file_size_limit
244 self.message = message
246 @property
247 def message(self):
248 return self.__message
250 @message.setter
251 def message(self, value):
252 if value is None:
253 self.__message = (
254 f'File size must be less than {self.file_size_limit}MB'
255 )
257 self.__message = value
259 def __call__(self, value):
260 if value.size > self.file_size_limit * 1024 * 1024:
261 raise ValidationError(self.message)