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
« 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
10# Modelo de los tipos de usuario
11class UserType(BaseModel):
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)
21 def __str__(self):
22 return self.name
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 ]
34# Extensiones admitidas para la foto de perfil.
35ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
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 ''
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'
49 return f"profile_pics/{uuid.uuid4()}.{ext}"
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")
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")
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 )
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()
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}$'
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)
88username_validator = RegexValidator(regex=USERNAME_REGEX, message=USERNAME_ERROR_MESSAGE)
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)
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 )
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 )
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 ]
140 def __str__(self):
141 return self.username
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)
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
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
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
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)
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)
185 read = models.BooleanField(default=False) # Indica si la notificacion ha sido leida o no
187 id_user = models.ForeignKey(
188 'Users',
189 on_delete=models.CASCADE,
190 db_column='id_user',
191 related_name='notifications'
192 )
194 class Meta:
195 db_table = 'notifications'
197 def __str__(self):
198 return self.content
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)
211 id_user = models.ForeignKey(
212 'Users',
213 on_delete=models.CASCADE,
214 db_column='id_user',
215 related_name='devices'
216 )
218 class Meta:
219 db_table = 'devices'
221 def __str__(self):
222 return f"{self.platform} device of {self.id_user}"
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)
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 ]
244 def __str__(self):
245 return self.description
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 )
268 class Meta:
269 db_table = 'preferences'
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 ]
290 def __str__(self):
291 return self.description
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 )
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 )
327 class Meta:
328 db_table = 'driver_ratings'
330# Modelo para los tipos de etiquetas ambientales de los vehiculos
331class EnvTypes(BaseModel):
333 id_type = models.AutoField(primary_key=True)
335 code = models.CharField(max_length=10, unique=True)
336 label = models.CharField(max_length=20)
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 ]
348 def __str__(self):
349 return self.label
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 )
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 )
371 id_user = models.ForeignKey(
372 'Users',
373 on_delete=models.CASCADE,
374 db_column='id_user',
375 related_name='vehicles'
376 )
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 )
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 ]
395 def __str__(self):
396 return self.license_plate