Coverage for users\models.py: 93.9%

152 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-30 20:58 +0200

1import uuid 

2from django.db import models 

3from api.base_models import BaseModel 

4from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator 

5from django.core.exceptions import ValidationError 

6from django.db.models import Q 

7from django.db.models.functions import Lower 

8from django.contrib.auth.hashers import make_password, check_password 

9 

10# Modelo de los tipos de usuario 

11class UserType(BaseModel): 

12 

13 id_type = models.AutoField( 

14 primary_key=True, 

15 editable=False, 

16 db_column='id_type' 

17 ) 

18 code = models.CharField(max_length=8, unique=True) 

19 name = models.CharField(max_length=50) 

20 

21 def __str__(self): 

22 return self.name 

23 

24 class Meta: 

25 db_table = 'user_type' 

26 constraints = [ 

27 models.UniqueConstraint( # El codigo solo es unico para los tipos que no estan borrados 

28 fields=['code'], 

29 condition=Q(is_deleted=False), 

30 name='unique_active_user_type_code' 

31 ) 

32 ] 

33 

34# Extensiones admitidas para la foto de perfil. 

35ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} 

36 

37 

38# Funcion para sacar la extension en minusculas, o cadena vacia si no tiene 

39def _extension_of(name): 

40 return name.rsplit('.', 1)[-1].lower() if '.' in name else '' 

41 

42 

43# Funcion para generar la ruta de las imagenes de perfil 

44def user_profile_path(instance, filename): 

45 ext = _extension_of(filename) 

46 if ext not in ALLOWED_EXTENSIONS: 

47 ext = 'jpg' 

48 

49 return f"profile_pics/{uuid.uuid4()}.{ext}" 

50 

51# Funcion para validar el tamaño de la imagen, para evitar subir archivos superiores a 2MB 

52def validate_size(file): 

53 max_size = 2 * 1024 * 1024 # 2 MB 

54 if file.size > max_size: 

55 raise ValidationError("La imagen es demasiado grande") 

56 

57# Funcion para validar que el archivo subido es una imagen 

58def validate_image(file): 

59 if not file.content_type.startswith('image/'): 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true

60 raise ValidationError("El archivo debe ser una imagen") 

61 

62# Funcion para validar la extension del fichero (ver ALLOWED_EXTENSIONS) 

63def validate_extension(file): 

64 if _extension_of(file.name) not in ALLOWED_EXTENSIONS: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 raise ValidationError( 

66 "Formato no admitido. Se aceptan: " 

67 + ", ".join(sorted(ALLOWED_EXTENSIONS)) 

68 ) 

69 

70# Funcion para dejar un correo en su forma canonica: sin espacios y en minusculas 

71def normalize_email(email): 

72 """ 

73 El correo se guarda y se busca siempre asi, en minusculas y sin espacios 

74 """ 

75 return (email or '').strip().lower() 

76 

77# Formato admitido para el nombre de usuario 

78# No se admiten las arrobas para que el nombre de usuario y el correo no se solapen  

79# Se prohiben ademas los espacios y la puntuacion suelta 

80USERNAME_REGEX = r'^[\w.-]{3,20}$' 

81 

82# Texto que se devuelve cuando el nombre de usuario no cumple el formato 

83USERNAME_ERROR_MESSAGE = ( 

84 "El nombre de usuario debe tener entre 3 y 20 caracteres y solo puede " 

85 "contener letras, numeros, puntos, guiones y guiones bajos" 

86) 

87 

88username_validator = RegexValidator(regex=USERNAME_REGEX, message=USERNAME_ERROR_MESSAGE) 

89 

90# Modelo de los usuarios 

91class Users(BaseModel): 

92 id = models.UUIDField( 

93 primary_key=True, 

94 default=uuid.uuid4, 

95 editable=False, 

96 db_column='id_user' 

97 ) 

98 username = models.CharField(max_length=20, validators=[username_validator]) 

99 email = models.EmailField(max_length=60) 

100 password = models.TextField(null=True, blank=True) # Podra ser null si el usuario se registra con Google 

101 name = models.CharField(max_length=50) 

102 surname1 = models.CharField(max_length=50) 

103 surname2 = models.CharField(max_length=50, null=True, blank=True) # El segundo apellido es opcional 

104 token = models.TextField(null=True, blank=True) 

105 token_expiration = models.DateTimeField(null=True, blank=True) 

106 token_purpose = models.CharField(max_length=20, null=True, blank=True) 

107 sessions_epoch = models.PositiveIntegerField(default=0) 

108 

109 profile_picture = models.ImageField( 

110 upload_to=user_profile_path, 

111 validators=[validate_image, validate_size, validate_extension], 

112 null=True, 

113 blank=True 

114 ) 

115 

116 user_type = models.ForeignKey( 

117 'UserType', 

118 to_field='code', 

119 on_delete=models.PROTECT, # No borrar en caso de borrar el UserType 

120 db_column='user_type' 

121 ) 

122 

123 is_verify = models.BooleanField(default=False) 

124 has_2FA = models.BooleanField(default=True) 

125 class Meta: 

126 db_table = 'users' 

127 constraints = [ 

128 models.UniqueConstraint( 

129 fields=['username'], # El nombre de usuario solo es unico para los usuarios que no estan borrados 

130 condition=Q(is_deleted=False), 

131 name='unique_active_username' 

132 ), 

133 models.UniqueConstraint( 

134 Lower('email'), 

135 condition=Q(is_deleted=False), 

136 name='unique_active_email' 

137 ) 

138 ] 

139 

140 def __str__(self): 

141 return self.username 

142 

143 # Se normaliza el correo antes de guardar 

144 def save(self, *args, **kwargs): 

145 """ 

146 Normaliza el correo antes de guardarlo, asi sirve tanto al crear como al actualizar 

147 """ 

148 self.email = normalize_email(self.email) 

149 super().save(*args, **kwargs) 

150 

151 # Propiedad para comprobar si el usuario esta autenticado, es decir, si su cuenta esta verificada y no esta borrada 

152 @property 

153 def is_authenticated(self): 

154 return self.is_verify and not self.is_deleted 

155 

156 # Propiedad para comprobar si el usuario esta activo, es decir, si no esta borrado 

157 @property 

158 def is_active(self): 

159 return not self.is_deleted 

160 

161 # Funcion para que cuando introduzca una contraseña, se guarde en bbdd como un hash y no en texto plano 

162 def set_password(self, raw_password): 

163 if raw_password: 

164 self.password = make_password(raw_password) 

165 else: 

166 self.password = None 

167 

168 # Funcion para comprobar si la contraseña introducida coincide con el hash almacenado en bbdd 

169 def check_password(self, raw_password): 

170 if not self.password or not raw_password: 

171 return False 

172 return check_password(raw_password, self.password) 

173 

174# Modelo de las notificaciones 

175class Notifications(BaseModel): 

176 id = models.UUIDField( 

177 primary_key=True, 

178 default=uuid.uuid4, 

179 editable=False, 

180 db_column='id' 

181 ) 

182 content = models.CharField(max_length=450) 

183 date = models.DateTimeField(auto_now_add=True) 

184 

185 read = models.BooleanField(default=False) # Indica si la notificacion ha sido leida o no 

186 

187 id_user = models.ForeignKey( 

188 'Users', 

189 on_delete=models.CASCADE, 

190 db_column='id_user', 

191 related_name='notifications' 

192 ) 

193 

194 class Meta: 

195 db_table = 'notifications' 

196 

197 def __str__(self): 

198 return self.content 

199 

200# Modelo para los dispositivos de los usuarios, usado para enviar notificaciones push via FCM 

201class Device(BaseModel): 

202 id = models.UUIDField( 

203 primary_key=True, 

204 default=uuid.uuid4, 

205 editable=False, 

206 db_column='id' 

207 ) 

208 fcm_token = models.CharField(max_length=255, unique=True) 

209 platform = models.CharField(max_length=20) 

210 

211 id_user = models.ForeignKey( 

212 'Users', 

213 on_delete=models.CASCADE, 

214 db_column='id_user', 

215 related_name='devices' 

216 ) 

217 

218 class Meta: 

219 db_table = 'devices' 

220 

221 def __str__(self): 

222 return f"{self.platform} device of {self.id_user}" 

223 

224# Modelo para los tipos de preferencias 

225class PrefTypes(BaseModel): 

226 id_pref = models.AutoField( 

227 primary_key=True, 

228 editable=False, 

229 db_column='id_pref' 

230 ) 

231 code = models.CharField(max_length=25, unique=True) 

232 description = models.CharField(max_length=50) 

233 

234 class Meta: 

235 db_table = 'pref_types' 

236 constraints = [ # El codigo solo es unico para los tipos de preferencias que no estan borrados 

237 models.UniqueConstraint( 

238 fields=['code'], 

239 condition=Q(is_deleted=False), 

240 name='unique_active_pref_types_code' 

241 ) 

242 ] 

243 

244 def __str__(self): 

245 return self.description 

246 

247# Modelo para las preferencias de los usuarios 

248class Preferences(BaseModel): 

249 id_pref = models.UUIDField( 

250 primary_key=True, 

251 default=uuid.uuid4, 

252 editable=False, 

253 db_column='id_pref' 

254 ) 

255 id_user = models.ForeignKey( 

256 'Users', 

257 on_delete=models.CASCADE, 

258 db_column='id_user', 

259 related_name='preferences' 

260 ) 

261 pref_type = models.ForeignKey( 

262 'PrefTypes', 

263 to_field='code', 

264 on_delete=models.CASCADE, 

265 db_column='pref_type' 

266 ) 

267 

268 class Meta: 

269 db_table = 'preferences' 

270 

271# Modelo para los criterios de valoracion de los conductores 

272class Criteria(BaseModel): 

273 id = models.AutoField( 

274 primary_key=True, 

275 editable=False, 

276 db_column='id' 

277 ) 

278 code = models.CharField(max_length=15, unique=True) 

279 description = models.CharField(max_length=100) 

280 class Meta: 

281 db_table = 'criteria' 

282 constraints = [ 

283 models.UniqueConstraint( # El codigo solo es unico para los criterios que no estan borrados 

284 fields=['code'], 

285 condition=Q(is_deleted=False), 

286 name='unique_active_criteria_code' 

287 ) 

288 ] 

289 

290 def __str__(self): 

291 return self.description 

292 

293# Modelo para las valoraciones de los conductores 

294class DriverRatings(BaseModel): 

295 id_rating = models.UUIDField( 

296 primary_key=True, 

297 default=uuid.uuid4, 

298 editable=False, 

299 db_column='id_rating' 

300 ) 

301 score = models.SmallIntegerField( 

302 validators=[ 

303 MinValueValidator(0), 

304 MaxValueValidator(10) 

305 ] 

306 ) 

307 id_user = models.ForeignKey( 

308 'Users', 

309 on_delete=models.CASCADE, 

310 db_column='id_user', 

311 related_name='ratings_given' 

312 ) 

313 

314 id_driver = models.ForeignKey( 

315 'Users', 

316 on_delete=models.CASCADE, 

317 db_column='id_driver', 

318 related_name='ratings_received' 

319 ) 

320 criteria = models.ForeignKey( 

321 'Criteria', 

322 to_field='code', 

323 on_delete=models.CASCADE, 

324 db_column='criteria' 

325 ) 

326 

327 class Meta: 

328 db_table = 'driver_ratings' 

329 

330# Modelo para los tipos de etiquetas ambientales de los vehiculos 

331class EnvTypes(BaseModel): 

332 

333 id_type = models.AutoField(primary_key=True) 

334 

335 code = models.CharField(max_length=10, unique=True) 

336 label = models.CharField(max_length=20) 

337 

338 class Meta: 

339 db_table = 'env_types' 

340 constraints = [ 

341 models.UniqueConstraint( # El codigo solo es unico para los tipos de etiquetas ambientales que no estan borradas 

342 fields=['code'], 

343 condition=Q(is_deleted=False), 

344 name='unique_active_env_code' 

345 ) 

346 ] 

347 

348 def __str__(self): 

349 return self.label 

350 

351# Modelo para los vehiculos 

352class Vehicles(BaseModel): 

353 id_vehicle = models.UUIDField( 

354 primary_key=True, 

355 default=uuid.uuid4, 

356 editable=False, 

357 db_column='id_vehicle' 

358 ) 

359 

360 brand = models.CharField(max_length=50) 

361 model = models.CharField(max_length=50) 

362 license_plate = models.CharField(max_length=10, unique=True) 

363 color = models.CharField(max_length=20) 

364 seats = models.SmallIntegerField( # El vehiculo debe tener entre 2 y 10 asientos 

365 validators=[ 

366 MinValueValidator(2), 

367 MaxValueValidator(10) 

368 ] 

369 ) 

370 

371 id_user = models.ForeignKey( 

372 'Users', 

373 on_delete=models.CASCADE, 

374 db_column='id_user', 

375 related_name='vehicles' 

376 ) 

377 

378 env_sticker = models.ForeignKey( 

379 'EnvTypes', 

380 on_delete=models.PROTECT, # Se evita borrar el tipo si hay un vehiculo con esa etiqueta 

381 to_field='code', 

382 db_column='env_sticker' 

383 ) 

384 

385 class Meta: 

386 db_table = 'vehicles' 

387 constraints = [ 

388 models.UniqueConstraint( # La matricula solo es unica para los vehiculos que no estan borrados 

389 fields=['license_plate'], 

390 condition=Q(is_deleted=False), 

391 name='unique_active_license_plate' 

392 ) 

393 ] 

394 

395 def __str__(self): 

396 return self.license_plate 

397