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):
Referències:
Podeu veure una mostra d'aquest projecte en producció a https://lliga.up.railway.app/
Aneu a l'article Django, creeu un projecte nou amb el seu Virtualenv.
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…
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.
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 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)
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:
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()
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:
inline
que mostri els Events
relacionats amb el partit.resultat
al PartitAdmin
que permeti comptabilitzar els gols (events) dels dos equips.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:
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)
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.
lliga/management/commands
:$ mkdir -p lliga/management/commands
Afegiu-hi el següent arxiu:
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.
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".
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à:
El template serà força senzill, una llista:
<ol> {% for equip in classificacio %} <li>{{equip.1}} ({{equip.0}} punts)</li> {% endfor %} </ol>
Ull, us caldrà afegir les funcions gols_local()
i gols_visitant()
al model de Partit
.
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, })
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:
<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>
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, })
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:
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:
<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:
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:
def classificacio(request, lliga_id): lliga = get_object_or_404( Lliga, pk=lliga_id) equips = lliga.equip_set.all() #...
Formularis
Elabora formularis per:
Workflow
Elabora el workflow per afegir un partit i les seves dades en temps real. Haurem de fer-ho en diversos passos:
/lliga/crea_partit/ /lliga/crea_partit/<lliga_id> /lliga/edita_partit/<partit_id>
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.
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.
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:
disabled
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.
Per saber més de com autenticar-nos pots llegir Django Auth