====== 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): - Introduir els equips i jugadors de cada lliga amb les seves dades complertes (dorsal, etc.). - Una pàgina per fer el seguiment d'un partit. Cada gol que es marqui s'introduirà com un event. - Una pàgina de resultats dels partits d'una lliga (llista ordenada per data de partit). - Una pàgina de resultats de partits (taula on es mostri local vs visitant). - Una taula de classificació per cada lliga, que compti els punts dels partits celebrats, gols a favor i en contra de cada equip. - Una taula de "pichichis" amb el rànking de golejadors de cada lliga. {{ futbol-chilena.png?300 }} Referències: * [[Django]] en aquesta wiki. * [[Virtualenv]] ídem. * [[Django Frontend]] ídem. {{tag> #Daw #DawMp07 #DawMp07Uf2 #DawMp07Uf02 django framework web }} \\ 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: * Lliga * Equip * Jugador * Partit És fàcil tenir la temptació de posar els resultats dins de l'objecte Partit 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 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: 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: * [[https://docs.djangoproject.com/en/stable/intro/tutorial02/|Django tutorial part 2 (models bàsics)]] * [[https://docs.djangoproject.com/en/stable/intro/tutorial07/|Django tutorial part 7 (customization i inlines)]]. Vigileu, però, la versió de la documentació que consulteu. Customitza les interfícies del //admin panel// per tal que puguis: * Partits: * Visualitzar a la llista: equip local, visitant, lliga i data. * Events de partits: * Visualitzar a la llista: (equip local + visitant), tipus, equip, jugador i temps (no data, només temps) * Partits: - Afegir un ''inline'' que mostri els ''Events'' relacionats amb el partit. - Afegir un mètode personalitzat ''resultat'' al ''PartitAdmin'' que permeti comptabilitzar els gols (events) dels dos equips. - Restringir les opcions que ofereix el //inline// dels events per tal que: * només mostri els jugadors dels dos equips del partit. * només mostri els dos equips del partit. {{ django:partit-event-inline.png }} --> Proposta PartitAdmin i EventInline# Per mostrar el //inline// i els resultats dels partits, aquest seria un possible codi: 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) <-- \\ ===== 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 [[https://faker.readthedocs.io/en/master/|per Python disposem de la llibreria Faker]] que ens facilitarà molt aquesta tasca. Podem crear el //seeder// dins una [[https://docs.djangoproject.com/en/stable/howto/custom-management-commands/|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# 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(titol=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( titol=titol_lliga, inici=timezone.now(), final=timezone.now()+timedelta(days=11*30)) 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) #print(equip) equip.save() lliga.equips.add(equip) print("Creem jugadors de l'equip "+nom) for j in range(25): nom = faker.first_name() cognom1 = faker.last_name() cognom2 = faker.last_name() jugador = Jugador(nom=nom,cognom1=cognom1,cognom2=cognom2,alias=nom+" "+cognom1) #print(jugador) jugador.save() fitxa = Fitxa(jugador=jugador,equip=equip,inici=timezone.now(),dorsal=i+1) fitxa.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 [[https://docs.djangoproject.com/en/stable/intro/tutorial03/|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 [[https://docs.djangoproject.com/en/stable/intro/tutorial01/#write-your-first-view|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à: - A la //view//, crea una llista d'equips amb la seva puntuació (cada element de la llista pot ser una tupla (punts,nom_equip)). - Ordena la llista de forma descendent. - Passa la llista al template, per a que la renderitzi. {{ django:lliga-classificacio.png?300 }} El //template// serà força senzill, una llista:
    {% for equip in classificacio %}
  1. {{equip.1}} ({{equip.0}} punts)
  2. {% endfor %}
--> Proposta view classificació# 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, }) <-- \\ ==== 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. {{ django:lliga-taula-partits.png }} Per [[https://stackoverflow.com/questions/17410058/django-how-to-render-a-matrix |renderitzar la matriu us recomano aquest aquest post]]. Us poso aquí el codi que es pot aplicar tal qual: {% for row in resultats %} {% for cell in row %} {% if forloop.first or forloop.parentloop.first %} {% endif %} {% endfor %} {% endfor %}
{% else %} {% endif %} {{ cell }} {% if forloop.first or forloop.parentloop.first %} {% else %}
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_frontend#formularis|Django Formularis]]. ==== Menu form ==== 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//. {{ django:menu_lliga.png?400 }} Ho farem amb l'objecte ''Form'' de Django: --> View i template per formulari de lliga# 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/ return redirect('classificacio',lliga.id) return render(request, "menu.html",{ "form": form, }) La plantilla o //template// ens quedaria així de simple:

Menu Lligues

{% csrf_token %} {{ form.as_p }}
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/", 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() #... <-- \\ ===== Exercicis ===== **Formularis** Elabora formularis per: - 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. - Crear equip. - 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: - Triar la lliga. - 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. - 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. - 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/edita_partit/ - 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 ==== {{ django:edita_partit_advanced.png?450 }} Crea els endpoints per a les APIs (recorda consultar [[Django API]]: * /api/get_lligues * /api/get_equips/ : ens filtrarà els equips que participen en aquesta lliga. Crea la pàgina ''edita_partit_advanced'' on hi hagi un formulari per editar un partit amb: * Selector de lliga * Selector d'equip local * Selector d'equip visitant * Botó "Editar": inicialment estarà ''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: * S'ha seleccionat el mateix equip local i visitant: error. * Si el partit ja existeix, no cal crear-ho. * Si el partit no existeix, es crea. 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]] \\