Skip to content

Módulo de Equipos

En esta sección detallamos la lógica de negocio de los equipos.

Modelos

Equipo

Bases: Model

Modelo que representa a un Equipo de Fútbol. Contiene información de identificación, colores, ubicación y fundación del equipo.

Source code in equipos/models.py
 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
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
class Equipo(models.Model):
    """
    Modelo que representa a un Equipo de Fútbol.
    Contiene información de identificación, colores, ubicación y fundación del equipo.
    """

    nombre = models.CharField(
        max_length=100, 
        unique=True, 
        verbose_name="Nombre del equipo"
    )

    slug = models.SlugField(unique=True, blank=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.nombre)
        super().save(*args, **kwargs)

    # Validador para asegurar que los colores sean códigos Hexadecimales (ej: #27a770)
    color_validator = RegexValidator(
        regex='^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$',
        message='Introduce un código de color Hexadecimal válido (ej: #27a770)',
    )

    color_principal = models.CharField(
        max_length=7, 
        default="#27a770", 
        validators=[color_validator],
        verbose_name="Color Principal"
    )

    color_secundario = models.CharField(
        max_length=7, 
        default="#504847", 
        validators=[color_validator],
        verbose_name="Color Secundario"
    )

    direccion = models.CharField(
        max_length=255, 
        blank=True, 
        null=True, 
        verbose_name="Dirección completa"
    )

    telefono = models.CharField(
        max_length=15, 
        blank=True, 
        null=True, 
        verbose_name="Teléfono de contacto"
    )

    escudo = models.ImageField(
        upload_to='escudos_equipos/', 
        blank=True, 
        null=True, 
        verbose_name="Escudo"
    )

    anio_fundacion = models.PositiveIntegerField(
        verbose_name="Año de fundación",
        validators=[
            MinValueValidator(1850), 
            MaxValueValidator(timezone.now().year)
        ],
        help_text="Año en el que se fundó el club"
    )

    # Metadatos extra (opcional, pero recomendado)
    fecha_creacion = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name = "Equipo"
        verbose_name_plural = "Equipos"
        ordering = ['nombre']

    def __str__(self):
        return self.nombre

EquipoEntrenador

Bases: Model

Modelo de relación N:N entre Equipo y PerfilEntrenador. Permite que un entrenador dirija múltiples equipos y que un equipo tenga múltiples entrenadores (técnico, asistente, etc.).

Source code in equipos/models.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class EquipoEntrenador(models.Model):
    """
    Modelo de relación N:N entre Equipo y PerfilEntrenador.
    Permite que un entrenador dirija múltiples equipos y 
    que un equipo tenga múltiples entrenadores (técnico, asistente, etc.).
    """
    equipo = models.ForeignKey(
        Equipo, 
        on_delete=models.CASCADE, 
        related_name='entrenadores'
    )

    perfil_entrenador = models.ForeignKey(
        PerfilEntrenador, 
        on_delete=models.CASCADE, 
        related_name='equipos'
    )

    fecha_incorporacion = models.DateField(
        auto_now_add=True,
        verbose_name="Fecha de incorporación"
    )

    fecha_salida = models.DateField(
        null=True,
        blank=True,
        verbose_name="Fecha de salida"
    )

    es_activo = models.BooleanField(
        default=True,
        verbose_name="¿Está activo en el equipo?"
    )

    class Meta:
        verbose_name = "Equipo - Entrenador"
        verbose_name_plural = "Equipos - Entrenadores"
        ordering = ['-fecha_incorporacion', 'equipo', 'perfil_entrenador']

    def __str__(self):
        return f"{self.perfil_entrenador.usuario.get_full_name()} en {self.equipo.nombre}"

EquipoJugador

Bases: Model

Modelo de relación N:N entre Equipo y PerfilJugador. Permite que un jugador pertenezca a múltiples equipos y que un equipo tenga múltiples jugadores.

Source code in equipos/models.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class EquipoJugador(models.Model):
    """
    Modelo de relación N:N entre Equipo y PerfilJugador.
    Permite que un jugador pertenezca a múltiples equipos y 
    que un equipo tenga múltiples jugadores.
    """
    equipo = models.ForeignKey(
        Equipo, 
        on_delete=models.CASCADE, 
        related_name='jugadores'
    )

    perfil_jugador = models.ForeignKey(
        PerfilJugador, 
        on_delete=models.CASCADE, 
        related_name='equipos'
    )

    fecha_incorporacion = models.DateField(
        auto_now_add=True,
        verbose_name="Fecha de incorporación"
    )

    fecha_salida = models.DateField(
        null=True,
        blank=True,
        verbose_name="Fecha de salida"
    )

    es_activo = models.BooleanField(
        default=True,
        verbose_name="¿Está activo en el equipo?"
    )

    class Meta:
        verbose_name = "Equipo - Jugador"
        verbose_name_plural = "Equipos - Jugadores"
        ordering = ['-fecha_incorporacion', 'equipo', 'perfil_jugador']

    def __str__(self):
        return f"{self.perfil_jugador.usuario.get_full_name()} en {self.equipo.nombre}"

Controladores

abandonar_equipo(request, equipo_id)

Permite a un jugador abandonar un equipo desactivando su relación con el equipo.

Source code in equipos/views.py
311
312
313
314
315
316
317
318
319
320
321
322
@login_required
def abandonar_equipo(request, equipo_id):
    """
    Permite a un jugador abandonar un equipo desactivando su relación con el equipo.
    """
    # Obtener el equipo y verificar que el usuario es el dueño (entrenador vinculado)
    equipo = get_object_or_404(Equipo, id=equipo_id)

    # Seguridad: Solo el entrenador del equipo puede borrarlo
    if request.user.perfil_entrenador.equipo != equipo:
        messages.error(request, "No tienes permiso para abandonar este equipo.")
        return redirect('landing')

agregar_jugador_equipo(request, equipo_id, jugador_id)

Agrega un jugador a un equipo. Solo el entrenador del equipo puede agregar jugadores.

Source code in equipos/views.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
@login_required
def agregar_jugador_equipo(request, equipo_id, jugador_id):
    """
    Agrega un jugador a un equipo.
    Solo el entrenador del equipo puede agregar jugadores.
    """
    if request.method == 'POST':
        equipo = get_object_or_404(Equipo, id=equipo_id)
        perfil_jugador = get_object_or_404(PerfilJugador, id=jugador_id)

        # Verificar que el usuario es entrenador de este equipo
        is_trainer = EquipoEntrenador.objects.filter(
            perfil_entrenador=request.user.perfil_entrenador,
            equipo=equipo,
            es_activo=True
        ).exists()

        if not is_trainer:
            return JsonResponse({'success': False, 'error': 'No tienes permiso'}, status=403)

        # Verificar si el jugador ya está ACTIVO en este equipo
        ya_existe_activo = EquipoJugador.objects.filter(
            equipo=equipo,
            perfil_jugador=perfil_jugador,
            es_activo=True
        ).exists()

        if ya_existe_activo:
            return JsonResponse({
                'success': False,
                'error': 'El jugador ya está en este equipo'
            })

        # Crear nuevo registro (permite múltiples periodos)
        equipo_jugador = EquipoJugador.objects.create(
            equipo=equipo,
            perfil_jugador=perfil_jugador,
            es_activo=True
        )

        # Actualizar el booleano tiene_equipo del usuario
        perfil_jugador.usuario.tiene_equipo = True
        perfil_jugador.usuario.save()

        return JsonResponse({
            'success': True,
            'message': f'{perfil_jugador.usuario.get_full_name()} añadido al equipo'
        })

    return JsonResponse({'success': False, 'error': 'Método no permitido'}, status=405)

crear_equipo(request)

Crea un nuevo equipo de fútbol. Solo accesible para entrenadores y administradores.

Source code in equipos/views.py
 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@login_required
@entrenador_o_admin_required
def crear_equipo(request):
    """
    Crea un nuevo equipo de fútbol.
    Solo accesible para entrenadores y administradores.
    """

    usuario = request.user

    if request.method == "POST":
        nombre = request.POST.get("nombre")
        anio_fundacion = request.POST.get("anio_fundacion")
        escudo = request.FILES.get("escudo")
        direccion = request.POST.get("direccion")
        telefono = request.POST.get("telefono")
        color_principal = request.POST.get("color_principal")
        color_secundario = request.POST.get("color_secundario")

        if not all([nombre, anio_fundacion, direccion, telefono]):
            messages.error(request, "Todos los campos son obligatorios.")
            return redirect("landing")

        equipo = Equipo(
            nombre=nombre,
            anio_fundacion=anio_fundacion,
            escudo=escudo,
            direccion=direccion,
            telefono=telefono,
            color_principal=color_principal,
            color_secundario=color_secundario
        )
        equipo.save()  # Esto ejecuta el método save() que genera el slug

        # Vincular al usuario como entrenador del equipo
        perfil_entrenador = PerfilEntrenador.objects.get_or_create(usuario=usuario)[0]
        EquipoEntrenador.objects.create(
            equipo=equipo,
            perfil_entrenador=perfil_entrenador
        )

        usuario.tiene_equipo = True
        usuario.save()

        messages.success(request, f"Equipo {equipo.nombre} creado correctamente.")

        # Recargar equipo para asegurar que tiene el slug
        equipo.refresh_from_db()
        return redirect('equipos:informacion_equipo', slug=equipo.slug)

    return render(request, "usuarios/inicio_entrenador_sin_equipo.html")

editar_datos_equipo(request, equipo_id)

Edita los datos básicos de un equipo (nombre, colors, dirección, teléfono, escudo). Solo el entrenador o administrador del equipo puede editar.

Source code in equipos/views.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
@login_required
def editar_datos_equipo(request, equipo_id):
    """
    Edita los datos básicos de un equipo (nombre, colors, dirección, teléfono, escudo).
    Solo el entrenador o administrador del equipo puede editar.
    """
    equipo = get_object_or_404(Equipo, id=equipo_id)

    if request.method == "POST":
        try:
            equipo.nombre = request.POST.get("nombre")
            equipo.anio_fundacion = request.POST.get("anio_fundacion")
            equipo.direccion = request.POST.get("direccion")
            equipo.telefono = request.POST.get("telefono")
            equipo.color_principal = request.POST.get("color_principal")
            equipo.color_secundario = request.POST.get("color_secundario")

            if "escudo" in request.FILES:
                equipo.escudo = request.FILES["escudo"]

            equipo.save()
            messages.success(request, "Datos del equipo actualizados correctamente")
            return redirect('equipos:informacion_equipo', slug=equipo.slug)

        except Exception as e:
            return render(request, 'equipos/informacion_equipo.html', {'equipo': equipo, 'error': f"Error al actualizar: {str(e)}"})

    return render(request, 'equipos/informacion_equipo.html', {'equipo': equipo})

editar_jugador(request, jugador_id)

Edita información de un jugador específico (dorsal y posición). Solo el entrenador del equipo donde el jugador está activo puede editar.

Source code in equipos/views.py
101
102
103
104
105
106
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
@login_required
@entrenador_o_admin_required
def editar_jugador(request, jugador_id):
    """
    Edita información de un jugador específico (dorsal y posición).
    Solo el entrenador del equipo donde el jugador está activo puede editar.
    """
    try:
        perfil_jugador = get_object_or_404(PerfilJugador, id=jugador_id)

        # Verificar que el usuario es entrenador de algún equipo del jugador
        es_entrenador = False
        for equipo_jugador in perfil_jugador.equipos.filter(es_activo=True):
            es_entrenador = EquipoEntrenador.objects.filter(
                perfil_entrenador=request.user.perfil_entrenador,
                equipo=equipo_jugador.equipo,
                es_activo=True
            ).exists()
            if es_entrenador:
                break

        if not es_entrenador:
            return JsonResponse({'success': False, 'error': 'No tienes permiso'}, status=403)

        # Actualizar los datos
        dorsal = request.POST.get('dorsal')
        altura = request.POST.get('altura')
        peso = request.POST.get('peso')
        pierna_habil = request.POST.get('pierna_habil')
        posicion = request.POST.get('posicion')
        es_capitan = request.POST.get('es_capitan') == 'on'

        if dorsal and dorsal.strip():
            perfil_jugador.dorsal = int(dorsal)
        if altura and altura.strip():
            perfil_jugador.altura = int(altura)
        if peso and peso.strip():
            perfil_jugador.peso = float(peso)
        if pierna_habil:
            perfil_jugador.pierna_habil = pierna_habil
        if posicion:
            perfil_jugador.posicion = posicion
        else:
            perfil_jugador.posicion = None

        perfil_jugador.es_capitan = es_capitan
        perfil_jugador.save()

        return JsonResponse({
            'success': True,
            'message': 'Información actualizada correctamente'
        })

    except Exception as e:
        return JsonResponse({'success': False, 'error': str(e)}, status=500)

eliminar_jugador_equipo(request, equipo_id, jugador_id)

Desactiva a un jugador de un equipo sin eliminar su perfil. Solo el entrenador del equipo puede ejecutar esta acción.

Source code in equipos/views.py
157
158
159
160
161
162
163
164
165
166
167
168
169
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
@login_required
def eliminar_jugador_equipo(request, equipo_id, jugador_id):
    """
    Desactiva a un jugador de un equipo sin eliminar su perfil.
    Solo el entrenador del equipo puede ejecutar esta acción.
    """
    """
    Marca un jugador como inactivo en el equipo (mantiene histórico).
    Solo el entrenador del equipo puede hacerlo.
    """
    try:
        # Obtener el equipo y el jugador
        equipo = get_object_or_404(Equipo, id=equipo_id)
        perfil_jugador = get_object_or_404(PerfilJugador, id=jugador_id)

        # Verificar que el usuario es entrenador del equipo
        es_entrenador = EquipoEntrenador.objects.filter(
            perfil_entrenador=request.user.perfil_entrenador,
            equipo=equipo,
            es_activo=True
        ).exists()

        if not es_entrenador:
            return JsonResponse({'success': False, 'error': 'No tienes permiso'}, status=403)

        # Marcar la relación como inactiva en lugar de eliminar
        equipo_jugador = EquipoJugador.objects.filter(
            equipo=equipo,
            perfil_jugador=perfil_jugador,
            es_activo=True
        ).first()

        if not equipo_jugador:
            return JsonResponse({'success': False, 'error': 'El jugador no está activo en este equipo'}, status=404)

        # Marcar como inactivo y guardar la fecha de salida
        from django.utils import timezone
        equipo_jugador.es_activo = False
        equipo_jugador.fecha_salida = timezone.now().date()
        equipo_jugador.save()

        # Verificar si el jugador está activo en otros equipos
        otros_equipos = EquipoJugador.objects.filter(
            perfil_jugador=perfil_jugador,
            es_activo=True
        ).count()

        # Si no está en otros equipos activos, actualizar tiene_equipo
        if otros_equipos == 0:
            perfil_jugador.usuario.tiene_equipo = False
            perfil_jugador.usuario.save()

        # Limpiar datos deportivos del equipo (dorsal y posición)
        perfil_jugador.dorsal = None
        perfil_jugador.posicion = None
        perfil_jugador.save()

        return JsonResponse({
            'success': True,
            'message': f'{perfil_jugador.usuario.get_full_name()} eliminado del equipo'
        })

    except Exception as e:
        return JsonResponse({'success': False, 'error': str(e)}, status=500)

informacion_equipo(request, slug)

Muestra la información detallada de un equipo incluyendo jugadores activos, entrenamientos y opciones de edición para el entrenador del equipo.

Source code in equipos/views.py
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
99
@login_required
def informacion_equipo(request, slug):
    """
    Muestra la información detallada de un equipo incluyendo jugadores activos,
    entrenamientos y opciones de edición para el entrenador del equipo.
    """
    equipo = get_object_or_404(Equipo, slug=slug)

    # Verificar si el usuario actual es entrenador de este equipo
    is_trainer = False
    if request.user.is_authenticated and request.user.rol == "entrenador":
        is_trainer = EquipoEntrenador.objects.filter(
            perfil_entrenador=request.user.perfil_entrenador,
            equipo=equipo,
            es_activo=True
        ).exists()

    # Obtener jugadores sin equipo
    jugadores_sin_equipo = PerfilJugador.objects.filter(
        usuario__tiene_equipo=False,
        usuario__rol='jugador'
    )

    # Obtener entrenador activo del equipo
    entrenador = equipo.entrenadores.filter(es_activo=True).first()

    # Obtener jugadores del equipo (activos)
    jugadores_equipo = equipo.jugadores.filter(es_activo=True).select_related(
        'perfil_jugador__usuario'
    ).order_by('perfil_jugador__usuario__first_name')

    context = {
        'equipo': equipo,
        'is_trainer': is_trainer,
        'jugadores_sin_equipo': jugadores_sin_equipo,
        'entrenador': entrenador,
        'jugadores_equipo': jugadores_equipo,
    }
    return render(request, 'equipos/informacion_equipo.html', context)

listado_equipos(request)

Muestra el listado de todos los equipos disponibles en la plataforma.

Source code in equipos/views.py
274
275
276
277
278
279
280
@login_required
def listado_equipos(request):
    """
    Muestra el listado de todos los equipos disponibles en la plataforma.
    """
    equipos = Equipo.objects.all()
    return render(request, "equipos/listado.html", {"equipos": equipos})

pizarra_tactica(request, slug)

Vista para la pizarra táctica del equipo. Solo los entrenadores activos del equipo pueden acceder.

Source code in equipos/views.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
@login_required
def pizarra_tactica(request, slug):
    """
    Vista para la pizarra táctica del equipo.
    Solo los entrenadores activos del equipo pueden acceder.
    """
    equipo = get_object_or_404(Equipo, slug=slug)

    # Verificar si el usuario es entrenador del equipo
    if request.user.rol != "entrenador":
        messages.error(request, "Solo los entrenadores pueden acceder a la pizarra táctica.")
        return redirect('landing')

    is_trainer = EquipoEntrenador.objects.filter(
        perfil_entrenador=request.user.perfil_entrenador,
        equipo=equipo,
        es_activo=True
    ).exists()

    if not is_trainer:
        messages.error(request, "No tienes permiso para acceder a la pizarra táctica de este equipo.")
        return redirect('landing')

    # Obtener jugadores del equipo (activos)
    jugadores_equipo = equipo.jugadores.filter(es_activo=True).select_related(
        'perfil_jugador__usuario'
    ).order_by('perfil_jugador__usuario__first_name')

    context = {
        'equipo': equipo,
        'jugadores': jugadores_equipo,
    }

    return render(request, 'equipos/pizarra_tactica.html', context)