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

1""" 

2This module contains custom Django validators for model field validation. 

3 

4The validators use the @deconstructible decorator to make them serializable 

5for Django migrations and form handling. 

6 

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

13 

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 _ 

18 

19import re 

20 

21 

22@deconstructible 

23class OnlyDigitsValidator: 

24 """ 

25 Validator that ensures a field contains only numeric characters. 

26 

27 This validator is used for fields like phone numbers where only 

28 digits are allowed. 

29 """ 

30 

31 # Default error message for this validator 

32 DEFAULT_MESSAGE = 'This field can contain only digits.' 

33 

34 def __init__(self, message=None): 

35 """ 

36 Initialize the validator with an optional custom message. 

37 """ 

38 self.message = message 

39 

40 @property 

41 def message(self): 

42 """ 

43 Get the current error message. 

44 """ 

45 return self.__message 

46 

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 

56 

57 def __call__(self, value): 

58 """ 

59 Validate that the value contains only digits. 

60 

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) 

67 

68 

69@deconstructible 

70class NameValidator: 

71 """ 

72 Validator that ensures a field contains only valid name characters. 

73 

74 This validator is used for name fields (first name, last name, city, country) 

75 and allows letters, hyphens, apostrophes, and spaces. 

76 """ 

77 

78 # Default error message for this validator 

79 DEFAULT_MESSAGE = ( 

80 'This field can contain only letters, hyphens, apostrophes and spaces.' 

81 ) 

82 

83 def __init__(self, message=None): 

84 """ 

85 Initialize the validator with an optional custom message. 

86 """ 

87 self.message = message 

88 

89 @property 

90 def message(self): 

91 """ 

92 Get the current error message. 

93 """ 

94 return self.__message 

95 

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 

105 

106 def __call__(self, value): 

107 """ 

108 Validate that the value contains only valid name characters. 

109 

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("'", "'") 

115 

116 for char in normalized: 

117 if not (char.isalpha() or char in "'-" or char.isspace()): 

118 raise ValidationError(self.__message) 

119 

120 

121@deconstructible 

122class UsernameValidator: 

123 """ 

124 Validator that ensures a username follows proper format rules. 

125 

126 This validator ensures usernames are 3-30 characters long and contain 

127 only letters, numbers, and underscores. 

128 """ 

129 

130 # Default error message for this validator 

131 DEFAULT_MESSAGE = 'Username must be 3-30 characters long and contain only letters, numbers, and underscores.' 

132 

133 def __init__(self, message=None): 

134 """ 

135 Initialize the validator with an optional custom message. 

136 

137 Args: 

138 message: Custom error message to use instead of default 

139 """ 

140 self.message = message 

141 

142 @property 

143 def message(self): 

144 """ 

145 Get the current error message. 

146 

147 Returns: 

148 str: The error message to display on validation failure 

149 """ 

150 return self.__message 

151 

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 

161 

162 def __call__(self, value): 

163 """ 

164 Validate that the value follows username format rules. 

165 

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 (_) 

169 

170 Args: 

171 value: The string value to validate 

172 

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) 

178 

179 

180@deconstructible 

181class EmailOrUsernameValidator: 

182 """ 

183 Validator that accepts either a valid email address or username. 

184 

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

189 

190 # Default error message for this validator 

191 DEFAULT_MESSAGE = 'Please enter a valid email address or username.' 

192 

193 def __init__(self, message=None): 

194 """ 

195 Initialize the validator with an optional custom message. 

196 """ 

197 self.message = message 

198 

199 @property 

200 def message(self): 

201 """ 

202 Get the current error message. 

203 """ 

204 return self.__message 

205 

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 

215 

216 def __call__(self, value): 

217 """ 

218 Validate that the value is either a valid email or username. 

219 

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() 

225 

226 is_email = False 

227 

228 try: 

229 django_email_validator(value) 

230 is_email = True 

231 except ValidationError: 

232 pass 

233 

234 is_username = re.match(r'^[A-Za-z0-9_]{3,30}$', value) 

235 

236 if not (is_email or is_username): 

237 raise ValidationError(self.__message) 

238 

239 

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 

245 

246 @property 

247 def message(self): 

248 return self.__message 

249 

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 ) 

256 

257 self.__message = value 

258 

259 def __call__(self, value): 

260 if value.size > self.file_size_limit * 1024 * 1024: 

261 raise ValidationError(self.message)