Taula de continguts

Exercici lliga de futbol en Django

El Mundo Deportivo ens encarrega una web per fer el seguiment de La Liga de 1a, 2a i 3a divisió.

La nostra empresa decideix realitzar el projecte utilitzant el framework web Django per minimitzar el temps de desenvolupament aprofitant el seu admin panel.

Ens ha de permetre (tingueu-ho en compte pel model):

  1. Introduir els equips i jugadors de cada lliga amb les seves dades complertes (dorsal, etc.).
  2. Una pàgina per fer el seguiment d'un partit. Cada gol que es marqui s'introduirà com un event.
  3. Una pàgina de resultats dels partits d'una lliga (llista ordenada per data de partit).
  4. Una pàgina de resultats de partits (taula on es mostri local vs visitant).
  5. Una taula de classificació per cada lliga, que compti els punts dels partits celebrats, gols a favor i en contra de cada equip.
  6. Una taula de «pichichis» amb el rànking de golejadors de cada lliga.

Referències:

, , , , , ,


Podeu veure una mostra d'aquest projecte en producció a https://lliga.up.railway.app/

Crear projecte

Aneu a l'article Django, creeu un projecte nou amb el seu Virtualenv.


Crear model

Els elements imprescindibles d'un model (a models.py) per a aquesta aplicació serien:

Creeu els models esmentats: Lliga, Equip, Jugador, Partit.

Podeu mirar el quickstart de l'article Django per iniciar el projecte i registrar els models a l'admin panel.

És fàcil tenir la temptació de posar els resultats dins de l'objecte Partit, però si volem un model més sofisticat seguiu llegint…

Millorant el model

Els resultats dels partits, si volem que tingui possibilitat de fer rànkings de «pichichi», no podem posar els resultats en el partit. Caldrà que tinguem un model on hi hagi Events (que podran ser de tipus gol, targeta groga, targeta vermella, falta, etc.) que estaran associats a un partit.

Proposta implementació Event
Mostrem aquí un exemple de Event que compta amb el tipus d'event, el partit, el jugador que el fa (després ens peremtrà comptar gols i fer la taula de «pichichis»), i l'equip que l'ha realitzat. Això darrer podria resultar redundant (i per tant, qüestionable), però ho implementem així per simplicitat de recompte de gols i, sobretot, perquè un jugador pot canviar d'equip en un moment donat, però el gol ha de comptar per a l'equip en el què jugava en aquell moment.
models.py
# models Lliga, Equip, Jugador, Partit...
 
class Event(models.Model):
    # el tipus d'event l'implementem amb algo tipus "enum"
    class EventType(models.TextChoices):
        GOL = "GOL"
        AUTOGOL = "AUTOGOL"
        FALTA = "FALTA"
        PENALTY = "PENALTY"
        MANS = "MANS"
        CESSIO = "CESSIO"
        FORA_DE_JOC = "FORA_DE_JOC"
        ASSISTENCIA = "ASSISTENCIA"
        TARGETA_GROGA = "TARGETA_GROGA"
        TARGETA_VERMELLA = "TARGETA_VERMELLA"
    partit = models.ForeignKey(Partit,on_delete=models.CASCADE)
    temps = models.TimeField()
    tipus = models.CharField(max_length=30,choices=EventType.choices)
    jugador = models.ForeignKey(Jugador,null=True,
                    on_delete=models.SET_NULL,
                    related_name="events_fets")
    equip = models.ForeignKey(Equip,null=True,
                    on_delete=models.SET_NULL)
    # per les faltes
    jugador2 = models.ForeignKey(Jugador,null=True,blank=True,
                    on_delete=models.SET_NULL,
                    related_name="events_rebuts")
    detalls = models.TextField(null=True,blank=True)
Proposta implementació Partit
Els partits no han de tenir els gols com a camp de dades. En realitat els gols els comptarem dels events que tindrà associats el Partit. Serà útil disposar de les funcions gols_local i gols_visitant que em facin la query i em comptabilitzin quants gols s'han donat al partit.

Això es pot implementar com segueix:

models.py
class Partit(models.Model):
    class Meta:
        unique_together = ["local","visitant","lliga"]
    local = models.ForeignKey(Equip,on_delete=models.CASCADE,
                    related_name="partits_local")
    visitant = models.ForeignKey(Equip,on_delete=models.CASCADE,
                    related_name="partits_visitant")
    lliga = models.ForeignKey(Lliga,on_delete=models.CASCADE)
    detalls = models.TextField(null=True,blank=True)
    inici = models.DateTimeField(null=True,blank=True)
    def __str__(self):
        return "{} - {}".format(self.local,self.visitant)
    def gols_local(self):
        return self.event_set.filter(
            tipus=Event.EventType.GOL,equip=self.local).count()
    def gols_visitant(self):
        return self.event_set.filter(
            tipus=Event.EventType.GOL,equip=self.visitant).count()


Implementar admin panel

Visualitza tots els models en el admin panel de Django.

Està explicat en els tutorials oficials Django, en particular a les parts:

Vigileu, però, la versió de la documentació que consulteu.

Customitza les interfícies del admin panel per tal que puguis:

Proposta PartitAdmin i EventInline
Per mostrar el inline i els resultats dels partits, aquest seria un possible codi:
admin.py
class EventInline(admin.TabularInline):
	model = Event
	fields = ["temps","tipus","jugador","equip"]
	ordering = ("temps",)
class PartitAdmin(admin.ModelAdmin):
        # podem fer cerques en els models relacionats
        # (noms dels equips o títol de la lliga)
	search_fields = ["local__nom","visitant__nom","lliga__titol"]
        # el camp personalitzat ("resultats" o recompte de gols)
        # el mostrem com a "readonly_field"
	readonly_fields = ["resultat",]
	list_display = ["local","visitant","resultat","lliga","inici"]
	ordering = ("-inici",)
	inlines = [EventInline,]
	def resultat(self,obj):
		gols_local = obj.event_set.filter(
		                tipus=Event.EventType.GOL,
                                equip=obj.local).count()
		gols_visit = obj.event_set.filter(
		                tipus=Event.EventType.GOL,
                                equip=obj.visitant).count()
		return "{} - {}".format(gols_local,gols_visit)
 
admin.site.register(Partit,PartitAdmin)

Podem millorar el EventInline restringint els jugadors que només ens mostri els dels equips del partit:

admin.py
class EventInline(admin.TabularInline):
    model = Event
    fields = ["temps","tipus","jugador","equip"]
    ordering = ("temps",)
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        # filtrem els jugadors i només deixem els que siguin d'algun dels 2 equips (local o visitant)
        if db_field.name == "jugador":
            partit_id = request.resolver_match.kwargs['object_id']
            partit = Partit.objects.get(id=partit_id)
            jugadors_local = [fitxa.jugador.id for fitxa in partit.local.fitxa_set.all()]
            jugadors_visitant = [fitxa.jugador.id for fitxa in partit.visitant.fitxa_set.all()]
            jugadors = jugadors_local + jugadors_visitant
            kwargs["queryset"] = Jugador.objects.filter(id__in=jugadors)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)            


Seeder per creació de dades d'exemple

Els seeder són programes que permeten la creació de dades falses per facilitar el test de l'aplicació.

En particular per Python disposem de la llibreria Faker que ens facilitarà molt aquesta tasca. Instal·leu-la amb:

(env) $ pip install faker

Podem crear el seeder dins una comanda personalitzada de Django que podrem cridar amb el manage.py tipus:

$ ./manage.py crea_lliga "Lliga fake 2"

L'argument «Lliga fake 2» serà el nom de la lliga que cal crear. La resta de dades les inventarem amb l'ajuda de Faker.

Proposta seeder crea_lliga
Creeu les carpetes lliga/management/commands:
$ mkdir -p lliga/management/commands

Afegiu-hi el següent arxiu:

lliga/management/commands/crea_lliga.py
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from faker import Faker
from datetime import timedelta
from random import randint
 
from lliga.models import *
 
faker = Faker(["es_CA","es_ES"])
 
class Command(BaseCommand):
    help = 'Crea una lliga amb equips i jugadors'
 
    def add_arguments(self, parser):
        parser.add_argument('titol_lliga', nargs=1, type=str)
 
    def handle(self, *args, **options):
        titol_lliga = options['titol_lliga'][0]
        lliga = Lliga.objects.filter(nom=titol_lliga)
        if lliga.count()>0:
            print("Aquesta lliga ja està creada. Posa un altre nom.")
            return
 
        print("Creem la nova lliga: {}".format(titol_lliga))
        lliga = Lliga( nom=titol_lliga, temporada="temporada" )
        lliga.save()
 
        print("Creem equips")
        prefixos = ["RCD", "Athletic", "", "Deportivo", "Unión Deportiva"]
        for i in range(20):
            ciutat = faker.city()
            prefix = prefixos[randint(0,len(prefixos)-1)]
            if prefix:
                prefix += " "
            nom =  prefix + ciutat
            equip = Equip(ciutat=ciutat,nom=nom,lliga=lliga)
            #print(equip)
            equip.save()
            lliga.equips.add(equip)
 
            print("Creem jugadors de l'equip "+nom)
            for j in range(25):
                nom = faker.name()
                posicio = "jugador"
                edat = 25
                jugador = Jugador(nom=nom,posicio=posicio,
                    edat=edat,equip=equip)
                #print(jugador)
                jugador.save()
 
        print("Creem partits de la lliga")
        for local in lliga.equips.all():
            for visitant in lliga.equips.all():
                if local!=visitant:
                    partit = Partit(local=local,visitant=visitant)
                    partit.local = local
                    partit.visitant = visitant
                    partit.lliga = lliga
                    partit.save()

Implementa el seeder i afegeix la creació de gols (Event.EventType.GOL) per tal de disposar de dades (resultats de partits) per elaborar posteriorment la classificació dels equips.


Crear frontend

Anem a crear les interfícies del frontend de l'aplicació. És a dir, les vistes personalitzades per les que ja no ens val utilitzar el admin panel.

Us pot servir de guia bàsica l'article Django Frontend i els tutorials oficials 3, 4 i 6 de Django.

Rutes dins la nostra app

Abans de res creeu dins la vostra app (per ex. «lliga») un arxiu lliga/urls.py i enllaceu-lo amb un include a lligaproject/urls.py.

Està explicat a Django tutorial part 1 "write your first view".


Taula classificació

En general, la feina de càlcul s'ha de fer a la view. El template és més lent en l'execució i no convé que hagi de calcular.

Per disposar d'una taula de classificació, una possible estratègia serà:

  1. A la view, crea una llista d'equips amb la seva puntuació (cada element de la llista pot ser una tupla (punts,nom_equip)).
  2. Ordena la llista de forma descendent.
  3. Passa la llista al template, per a que la renderitzi.

El template serà força senzill, una llista:

classificacio.html
<ol>
{% for equip in classificacio %}
	<li>{{equip.1}} ({{equip.0}} punts)</li>
{% endfor %}
</ol>
Proposta view classificació

Ull, us caldrà afegir les funcions gols_local() i gols_visitant() al model de Partit.

views.py
def classificacio(request):
    lliga = Lliga.objects.first()
    equips = lliga.equips.all()
    classi = []
 
    # calculem punts en llista de tuples (equip,punts)
    for equip in equips:
        punts = 0
        for partit in lliga.partit_set.filter(local=equip):
            if partit.gols_local() > partit.gols_visitant():
                punts += 3
            elif partit.gols_local() == partit.gols_visitant():
                punts += 1
        for partit in lliga.partit_set.filter(visitant=equip):
            if partit.gols_local() < partit.gols_visitant():
                punts += 3
            elif partit.gols_local() == partit.gols_visitant():
                punts += 1
        classi.append( (punts,equip.nom) )
    # ordenem llista
    classi.sort(reverse=True)
    return render(request,"classificacio.html",
                {
                    "classificacio":classi,
                })


Taula local vs visitant

Per la taula de partits locals vs visitants la cosa es complica. Haurem de crear una matriu amb les dades dels partits.

Per renderitzar la matriu us recomano aquest aquest post. Us poso aquí el codi que es pot aplicar tal qual:

taula_partits.html
<table>
    {% for row in resultats %}
    <tr>
    {% for cell in row %}
        {% if forloop.first or forloop.parentloop.first %} <th> {% else %} <td> {% endif %}
            {{ cell }}
        {% if forloop.first or forloop.parentloop.first %} </th> {% else %} </td> {% endif %}
    {% endfor %}
    </tr>
    {% endfor %}
</table>
views.py
def taula_partits(request):
    # per mostrar la taula de partits, creem una matriu (resultats)
    # per renderitzar-la després de forma senzilla a la view
    lliga = Lliga.objects.first()
    equips = [ equip.nom for equip in lliga.equips.order_by("nom") ]
    resultats = []
    resultats.append( [""] + equips )
    for local in equips:
        local_res = [local,]
        for visitant in equips:
            if local!=visitant:
                partit = lliga.partit_set.filter(local__nom=local,visitant__nom=visitant)
                if not partit:
                    local_res.append("")
                else:
                    local_res.append(str(partit.get().gols_local())+"-"+str(partit.get().gols_visitant()))
            else:
                local_res.append("x")
        resultats.append(local_res)
 
    # renderitzem una taula genèrica, trobat a:
    # https://stackoverflow.com/questions/17410058/django-how-to-render-a-matrix
    return render(request,"taula_partits.html",
                {
                    "lliga":lliga,
                    "equips":equips,
                    "resultats": resultats,
                })


Formularis

Podeu llegir referències sobre els potents formularis de Django a Django Formularis.

Anem a fer un menú que ens permeti triar la lliga entre les lligues que tenim a la BD. Per tant, es tracta d'un camp depenent de les dades i no pot ser hardcoded.

Ho farem amb l'objecte Form de Django:

View i template per formulari de lliga
views.py
from django import forms
from django.shortcuts import redirect
 
from lliga.models import *
 
class MenuForm(forms.Form):
    lliga = forms.ModelChoiceField(queryset=Lliga.objects.all())
 
def menu(request):
    form = MenuForm()
    if request.method == "POST":
        form = MenuForm(request.POST)
        if form.is_valid():
            lliga = form.cleaned_data.get("lliga")
            # cridem a /classificacio/<lliga_id>
            return redirect('classificacio',lliga.id)
    return render(request, "menu.html",{
                    "form": form,
            })

La plantilla o template ens quedaria així de simple:

menu.html
<h1>Menu Lligues</h1>
 
<form method="post">
	{% csrf_token %}
	{{ form.as_p }}
 
	<input type="submit" name="submit">
</form>

També necessitarem modificar les URLs per poder accedir adequadament a la classificació i passar-li el ID de la lliga que volem visualitzar:

urls.py
urlpatterns = [
    path("menu", views.menu, name="menu"),
    path("classificacio/<int:lliga_id>", views.classificacio, name="classificacio"),
]

Finalment modifiquem views.py per tal que rebem el paràmetre lliga_id i filtrem la lliga que volem mostrar:

views.py
def classificacio(request, lliga_id):
    lliga = get_object_or_404( Lliga, pk=lliga_id)
    equips = lliga.equip_set.all()
    #...


Exercicis

Formularis

Elabora formularis per:

  1. Crear lliga.
    • Assegura't que si ja hi ha una lliga amb el mateix nom, no ens deixi guardar.
    • Pots fer-ho amb Form o amb ModelForm.
  2. Crear equip.
  3. Assignar equips a una lliga (afegir o treure).

Workflow

Elabora el workflow per afegir un partit i les seves dades en temps real. Haurem de fer-ho en diversos passos:

  1. Triar la lliga.
  2. Triar els dos equips que s'enfronten (només oferirà els equips de la lliga triada). Si el partit ja està introduït, ha d'avisar i tornar-te a demanar seleccionar-los.
  3. Formulari de partit: hem de poder introduir Events, siguin gols, targetes, etc. i amb totes les dades necessàries (el partit és fix). Només ens deixarà triar entre els equips i jugadors dels dos equips implicats.
  4. Una opció per resoldre aquest exercici és utilitzant rutes que indiquin el ID de la lliga i després la ID del partit, per exemple:
    /lliga/crea_partit/
    /lliga/crea_partit/<lliga_id>
    /lliga/edita_partit/<partit_id>
  5. Protegeix les rutes per tal que només puguin entrar usuaris loguejats.

View avançada

Elabora una pàgina de visualització d'un partit en temps real. Ens mostrarà el resultat actual i a continuació una llista amb els Events que van sortint en temps real. Caldrà refrescar el resultat un cop cada 5 segons. Pots implementar crides AJAX com es descriu a Django API.


Integrant AJAX en Django

Per poder utilitzar AJAX en Django caldrà disposar d'una API, pots consultar com fer-ho a Django API.

Les crides AJAX caldrà implementar-les dins el codi JS, que en el cas de Django ha d'anar dins els arxius estàtics o dins la pròpia plantilla HTML.

Exercicis

Crea els endpoints per a les APIs (recorda consultar Django API:

Crea la pàgina edita_partit_advanced on hi hagi un formulari per editar un partit amb:

Tots els selectors estan buits inicialment. Els omplirem utilitzant les APIs abans creades.

El selector de lliga es crearà immediatament al carregar la pàgina. Els selectors d'equips es carregaran de dades quan es seleccioni lliga.

Un cop seleccionem la lliga i els equips, pots habilitar el botó «Editar» per tal que iniciem l'entrada de dades del partit.

Fes un control d'errors o de casos:

Finalment oferim el link per a editar el partit (afegir Events) en una nova view.


Autenticació

Per saber més de com autenticar-nos pots llegir Django Auth