Skip to content

Módulo de Usuarios

Gestión de autenticación y perfiles de usuario.

Modelos

PerfilEntrenador

Bases: Model

Datos específicos para usuarios con rol de ENTRENADOR.

Source code in usuarios/models.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class PerfilEntrenador(models.Model):
    """
    Datos específicos para usuarios con rol de ENTRENADOR.
    """
    usuario = models.OneToOneField(
        Usuario, 
        on_delete=models.CASCADE, 
        related_name='perfil_entrenador'
    )

    licencia = models.CharField(_('número de licencia'), max_length=50, blank=True, null=True)
    experiencia_anos = models.PositiveIntegerField(_('años de experiencia'), default=0)

    def get_equipos_activos(self):
        """Devuelve solo los equipos donde el entrenador está activo."""
        return self.equipos.filter(es_activo=True)

    def __str__(self):
        return f"Perfil Entrenador: {self.usuario.get_full_name()}"

get_equipos_activos()

Devuelve solo los equipos donde el entrenador está activo.

Source code in usuarios/models.py
114
115
116
def get_equipos_activos(self):
    """Devuelve solo los equipos donde el entrenador está activo."""
    return self.equipos.filter(es_activo=True)

PerfilJugador

Bases: Model

Datos específicos para usuarios con rol de JUGADOR.

Source code in usuarios/models.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class PerfilJugador(models.Model):
    """
    Datos específicos para usuarios con rol de JUGADOR.
    """
    usuario = models.OneToOneField(
        Usuario, 
        on_delete=models.CASCADE, 
        related_name='perfil_jugador'
    )

    altura = models.PositiveIntegerField(_('altura (cm)'), null=True, blank=True)
    peso = models.DecimalField(_('peso (kg)'), max_digits=5, decimal_places=2, null=True, blank=True)
    dorsal = models.PositiveIntegerField(_('dorsal'), null=True, blank=True)

    class PiernaHabil(models.TextChoices):
        DERECHA = 'derecha', _('Derecha')
        IZQUIERDA = 'izquierda', _('Izquierda')
        AMBAS = 'ambas', _('Ambas')

    pierna_habil = models.CharField(
        max_length=10, 
        choices=PiernaHabil.choices, 
        default=PiernaHabil.DERECHA
    )

    class Posicion(models.TextChoices):
        PORTERO = 'portero', _('Portero')
        LATERAL_DERECHO = 'lateral_derecho', _('Lateral Derecho')
        DEFENSA_CENTRAL = 'defensa_central', _('Defensa Central')
        LATERAL_IZQUIERDO = 'lateral_izquierdo', _('Lateral Izquierdo')
        MEDIOCENTRO_DEFENSIVO = 'mediocentro_defensivo', _('Mediocentro Defensivo')
        MEDIOCENTRO = 'mediocentro', _('Mediocentro')
        MEDIOCENTRO_OFENSIVO = 'mediocentro_ofensivo', _('Mediocentro Ofensivo')
        INTERIOR_IZQUIERDA = 'interior_izquierda', _('Interior Izquierda')
        INTERIOR_DERECHA = 'interior_derecha', _('Interior Derecha')
        EXTREMO_DERECHO = 'extremo_derecho', _('Extremo Derecho')
        EXTREMO_IZQUIERDO = 'extremo_izquierdo', _('Extremo Izquierdo')
        SEGUNDO_DELANTERO = 'segundo_delantero', _('Segundo Delantero')
        DELANTERO_CENTRO = 'delantero_centro', _('Delantero Centro')

    posicion = models.CharField(
        max_length=25,
        choices=Posicion.choices,
        null=True,
        blank=True,
        help_text="Posición en el equipo actual (asignada por el entrenador)"
    )

    es_capitan = models.BooleanField(_('es capitán'), default=False)

    def get_equipos_activos(self):
        """Devuelve solo los equipos donde el jugador está activo."""
        return self.equipos.filter(es_activo=True)

    def __str__(self):
        return f"Perfil Jugador: {self.usuario.get_full_name()}"

get_equipos_activos()

Devuelve solo los equipos donde el jugador está activo.

Source code in usuarios/models.py
93
94
95
def get_equipos_activos(self):
    """Devuelve solo los equipos donde el jugador está activo."""
    return self.equipos.filter(es_activo=True)

Usuario

Bases: AbstractUser

Modelo de usuario personalizado que extiende de AbstractUser. Sustituye al modelo auth.User por defecto de Django.

Source code in usuarios/models.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Usuario(AbstractUser):
    """
    Modelo de usuario personalizado que extiende de AbstractUser.
    Sustituye al modelo auth.User por defecto de Django.
    """

    # Definimos los roles posibles
    class Rol(models.TextChoices):
        ADMIN = 'admin', _('Administrador')
        ENTRENADOR = 'entrenador', _('Entrenador')
        JUGADOR = 'jugador', _('Jugador')
        INVITADO = 'invitado', _('Invitado')

    # Campos adicionales comunes a todos los usuarios
    email = models.EmailField(_('dirección de correo electrónico'), unique=True)
    telefono = models.CharField(_('teléfono'), max_length=15, blank=True, null=True)
    fecha_nacimiento = models.DateField(_('fecha de nacimiento'), blank=True, null=True)
    foto = models.ImageField(upload_to='fotos_perfil/', blank=True, null=True)
    tiene_equipo = models.BooleanField(default=False)
    slug = models.SlugField(max_length=255, unique=True, null=True, blank=True)
    rol = models.CharField(
        max_length=20, 
        choices=Rol.choices, 
        default=Rol.INVITADO
    )

    # Usamos el email como identificador principal en lugar del username
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username', 'first_name', 'last_name']

    class Meta:
        verbose_name = _('Usuario')
        verbose_name_plural = _('Usuarios')

    def __str__(self):
        return f"{self.get_full_name()} ({self.get_rol_display()})"

Controladores

CustomPasswordResetConfirmView

Bases: PasswordResetConfirmView

Vista personalizada para confirmar y establecer una nueva contraseña. Usa templates personalizados según la interfaz de la aplicación.

Source code in usuarios/views.py
207
208
209
210
211
212
213
class CustomPasswordResetConfirmView(PasswordResetConfirmView):
    """
    Vista personalizada para confirmar y establecer una nueva contraseña.
    Usa templates personalizados según la interfaz de la aplicación.
    """
    template_name = "usuarios/new_password.html"
    success_url = reverse_lazy("usuarios:new_password_done")

ResetPasswordView

Bases: SuccessMessageMixin, PasswordResetView

Vista personalizada para solicitar reinicio de contraseña. Si el usuario está autenticado, envía el correo directamente. Si no, muestra el formulario para ingresar el email.

Source code in usuarios/views.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
class ResetPasswordView(SuccessMessageMixin, PasswordResetView):
    """
    Vista personalizada para solicitar reinicio de contraseña.
    Si el usuario está autenticado, envía el correo directamente.
    Si no, muestra el formulario para ingresar el email.
    """
    template_name = 'usuarios/password_forget.html'
    email_template_name = 'usuarios/password_forget_email.html'
    subject_template_name = 'usuarios/password_forget_subject.txt'
    success_url = reverse_lazy('usuarios:password_forget_done')
    html_email_template_name = 'usuarios/password_forget_email.html'

    def dispatch(self, request, *args, **kwargs):
        # Si el usuario YA está logueado, enviamos el correo directamente
        if request.user.is_authenticated:
            form = PasswordResetForm({'email': request.user.email})

            if form.is_valid():
                form.save(
                    request=request,
                    use_https=request.is_secure(),
                    subject_template_name = self.subject_template_name,
                    email_template_name=self.email_template_name,
                    html_email_template_name=self.html_email_template_name,
                )

                messages.success(
                    request,
                    _("Te hemos enviado un correo para cambiar tu contraseña.")
                )

                return redirect("usuarios:miperfil")

        # Si NO está logueado, seguimos el flujo normal de Django
        return super().dispatch(request, *args, **kwargs)

eliminar_cuenta(request)

Elimina la cuenta del usuario autenticado de manera permanente. Cierra la sesión antes de eliminar el usuario de la base de datos.

Source code in usuarios/views.py
156
157
158
159
160
161
162
163
164
165
166
167
168
@login_required
def eliminar_cuenta(request):
    """
    Elimina la cuenta del usuario autenticado de manera permanente.
    Cierra la sesión antes de eliminar el usuario de la base de datos.
    """
    if request.method == "POST":
        user = request.user
        logout(request)          # cerrar sesión primero
        user.delete()            # eliminar usuario
        return JsonResponse({"success": True})

    return JsonResponse({"success": False}, status=400)

login_view(request)

Autentica a un usuario con email y contraseña. Inicia sesión y redirige a la página principal si tiene éxito.

Source code in usuarios/views.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def login_view(request):
    """
    Autentica a un usuario con email y contraseña.
    Inicia sesión y redirige a la página principal si tiene éxito.
    """
    if request.method == "GET":
        return render(request, 'usuarios/login.html')

    # POST → procesar login
    email = request.POST.get('email')
    password = request.POST.get('password')

    # Autenticación
    user = authenticate(request, username=email, password=password)

    if user is not None:
        login(request, user)
        messages.success(request, _("Sesión inciada correctamente."))
        return render(request, "usuarios/login.html")  # o dashboard
    else:
        return render(request, 'usuarios/login.html', {'error': _("El usuario no existe o la contraseña es incorrecta."), "email_value": email})

logout_view(request)

Cierra la sesión del usuario autenticado y redirige a la página de inicio.

Source code in usuarios/views.py
 99
100
101
102
103
104
105
@login_required
def logout_view(request):
    """
    Cierra la sesión del usuario autenticado y redirige a la página de inicio.
    """
    logout(request)
    return redirect('landing')

miperfil(request)

Ver y editar mi propio perfil

Source code in usuarios/views.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@login_required
def miperfil(request):
    """Ver y editar mi propio perfil"""
    user = request.user

    try:
        if request.method == "POST":
            user.first_name = request.POST.get("first_name")
            user.last_name = request.POST.get("last_name")
            user.telefono = request.POST.get("telefono")
            user.fecha_nacimiento = request.POST.get("fecha_nacimiento")

            if "foto" in request.FILES:
                user.foto = request.FILES["foto"]

            user.save()
            messages.success(request, _("Perfil actualizado correctamente"))
            return redirect("usuarios:miperfil")

        context = {
            'usuario_perfil': user,
            'es_mi_perfil': True,
        }
        return render(request, "usuarios/miperfil.html", context)
    except Exception as e:
        return render(request, 'usuarios/miperfil.html', {'error': _("Error al actualizar: {}").format(str(e))})

signin(request)

Registra un nuevo usuario en la plataforma. Crea el usuario y sus perfiles asociados (jugador o entrenador) según el tipo seleccionado.

Source code in usuarios/views.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def signin(request):
    """
    Registra un nuevo usuario en la plataforma.
    Crea el usuario y sus perfiles asociados (jugador o entrenador) según el tipo seleccionado.
    """
    if request.method == "GET":
        return render(request, 'usuarios/signin.html')
    else:
        # Obtener datos del formulario
        nombre = request.POST.get('nombre')
        apellidos = request.POST.get('apellidos')
        telefono = request.POST.get('telefono')
        email = request.POST.get('email')
        fechanacimiento = request.POST.get('fechanacimiento')
        password_1 = request.POST.get('password1')
        password_2 = request.POST.get('password2')
        es_entrenador = request.POST.get('es_entrenador') 
        # Archivos: Las imágenes van en request.FILES, no en POST
        foto = request.FILES.get('foto') 

        if password_1 != password_2:
            return render(request, 'usuarios/signin.html', {'error': _("Las contraseñas no coinciden."), 'nombre_value': nombre, 'apellidos_value': apellidos, 'telefono_value': telefono, 'email_value': email, 'fechanacimiento_value': fechanacimiento, 'es_entrenador_value': es_entrenador, 'foto_value': foto})

        if Usuario.objects.filter(email=email).exists():
            return render(request, 'usuarios/signin.html', {'error': _("Este correo electrónico ya está registrado."), 'nombre_value': nombre, 'apellidos_value': apellidos, 'telefono_value': telefono, 'email_value': email, 'fechanacimiento_value': fechanacimiento, 'es_entrenador_value': es_entrenador, 'foto_value': foto})

        # Crear el Usuario
        try:
            # Usamos 'create_user' que se encarga de encriptar la contraseña automáticamente
            user = Usuario.objects.create_user(
                username=email, # Usamos el email como username interno
                first_name=nombre,
                last_name=apellidos,
                telefono=telefono,
                fecha_nacimiento=fechanacimiento,
                email=email,
                password=password_1,
                foto=foto
            )

            # Asignar Rol y Crear Perfil Específico
            if es_entrenador:
                user.rol = Usuario.Rol.ENTRENADOR
                # Creamos la ficha de entrenador vacía asociada a este usuario
                PerfilEntrenador.objects.create(usuario=user)
            else:
                user.rol = Usuario.Rol.JUGADOR
                # Creamos la ficha de jugador vacía asociada a este usuario
                PerfilJugador.objects.create(usuario=user)

            user.save()

            # Iniciar sesión automáticamente y redirigir
            login(request, user)
            messages.success(request, _("Te has registrado correctamente"))
            # Redirigir a la página de inicio
            return redirect('landing')

        except Exception as e:
            # Si pasa algo raro, mostramos el error
            return render(request, 'usuarios/signin.html', {'error': _("Error al registrar: {}").format(str(e))})

ver_perfil_usuario(request, slug)

Ver el perfil de otro usuario usando slug

Source code in usuarios/views.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
@login_required
def ver_perfil_usuario(request, slug):
    """Ver el perfil de otro usuario usando slug"""
    usuario = get_object_or_404(Usuario, slug=slug)

    # Obtener información adicional según el rol
    perfil_jugador = None
    perfil_entrenador = None

    if usuario.rol == 'jugador':
        perfil_jugador = usuario.perfil_jugador
    elif usuario.rol == 'entrenador':
        perfil_entrenador = usuario.perfil_entrenador

    context = {
        'usuario_perfil': usuario,
        'perfil_jugador': perfil_jugador,
        'perfil_entrenador': perfil_entrenador,
        'es_mi_perfil': usuario == request.user,
    }
    return render(request, 'usuarios/miperfil.html', context)