Salta el contingut

UD6: MULTICAPA - VISTES I CONTROLADORS, ESDEVENIMENTS I INTERACTIVITAT

INTRODUCCIÓ

En aquesta unitat introduirem la creació de formularis amb Django per tal de manipular les dades de la nostra aplicació web. Pel camí descobrirem nous tipus de vistes (CBV) que automatitzen moltes de les operacions que hem de realitzar. També aprendrem a utilitzar apps (o mòduls) desenvolupats per tercers per personalitzar més convenientment els nostres formularis i afegir interactivitat des del servidor.

Tot això ho articularem mitjançant l'exemple guiat del portfolio.

Quant a les activitats, introduirem l'ús de formularis al projecte iniciat en la unitat anterior, entre altres aspectes.

AVALUACIÓ

El present document, juntament amb el seu corresponent butlletí d'activitats (publicat addicionalment), cobreix els següents criteris d'avaluació:

RESULTATS D'APRENENTATGE CRITERIS D'AVALUACIÓ
RA5. Desenvolupa aplicacions Web identificant i aplicant mecanismes per a separar el codi de presentació de la lògica de negoci. c) S'han utilitzat objectes i controls en el servidor per a generar l'aspecte visual de l'aplicació web en el client.
d) S'han utilitzat formularis generats de manera dinàmica per a respondre als esdeveniments de l'aplicació Web.
h) S'ha provat i documentat el codi.
RA6. Desenvolupa aplicacions web d'accés a magatzems de dades, aplicant mesures per a mantindre la seguretat i la integritat de la informació f) S'han creat aplicacions web que permeten l'actualització i l'eliminació d'informació disponible en una base de dades.
RA8. Genera pàgines web dinàmiques analitzant i utilitzant tecnologies i frameworks del servidor web que afegeixen codi al llenguatge de marques. a) S'han identificat les diferències entre l'execució de codi en el servidor i en el client web.
b) S'han reconegut els avantatges d'unir ambdues tecnologies en el procés de desenvolupament de programes.
c) S'han identificat les tecnologies i frameworks relacionades amb la generació per part del servidor de pàgines web amb guions incrustats.
d) S'han utilitzat aquestes tecnologies i frameworks per a generar pàgines web que inclouen interacció amb l'usuari.
e) S'han utilitzat aquestes tecnologies i frameworks, per a generar pàgines web que inclouen verificació de formularis.
f) S'han utilitzat aquestes tecnologies i frameworks per a generar pàgines web que inclouen modificació dinàmica del seu contingut i la seua estructura.
g) S'han aplicat aquestes tecnologies i frameworks en la programació d'aplicacions web.

Vistes d'edició genèriques

En la unitat anterior vam veure que existeixen dos tipus de vistes: FBV i CBV. En aquesta unitat aprofundirem més en les segones.

CreateView

Aquesta vista crea un formulari per a nosaltres per a la creació d'un objecte, i s'encarrega també de totes les validacions i els seus corresponents missatges.

Anem a crear una vista d'aquest tipus per a crear nous projectes en el portfolio. En views.py, inserim el següent codi, per a la creació de nous projectes (importem abans la classe CreateView):

class ProyectoCreateView(CreateView):
    model = Proyecto
    fields = ['titulo', 'descripcion', 'fecha_creacion', 'year', 'categorias', 'imagen']
A continuació creem una plantilla en una carpeta anomenada "portfolioapp", dins de la carpeta "templates", amb el nom "proyecto_form.html", amb el següent contingut:
{% extends 'portfolio/base.html' %}

{% block content %}
<div class="container">
    <h2>Creació d'un nou projecte</h2>
    <form method="post" enctype="multipart/form-data">{% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Guardar">
    </form>
</div>
{% endblock %}
ACTIVITAT: investiga què significa csrf_token i per què l'hem d'incloure sempre al crear un formulari.

Finalment, creem una nova URL:

path('proyecto_create/', views.ProyectoCreateView.as_view(), name='proyecto_create')

Anem al navegador, i introduïm la URL segons l'estructura que hem configurat i, obtenim el formulari amb tots els seus camps!

Emocionant, però què ha passat darrere les cortines perquè amb tan poques línies hàgem obtingut un formulari funcional? Ha ocorregut el següent:

  • CreateView ha derivat un formulari basant-se en el model Proyecto, prenent els camps que li hem passat en la llista fields.
  • Hem utilitzat aquest formulari en el template "proyecto_form.html". La vista se l'ha passat automàticament per context, sense que nosaltres hàgim configurat res.
  • Com sap Django quina plantilla ha d'utilitzar per a la nova vista? Ho sap perquè hem deixat la plantilla sota el directori "portfolioapp" en templates, amb el mateix nom que l'app que conté la vista. A més, hem seguit la nomenclatura [nom model]_form.html, és a dir projecte_form.html, amb la qual cosa Django ho ha sabut trobar.

Anem a crear el nostre primer projecte. Premem en Guardar, i obtenim el següent:

Django ens avisa que no hem configurat la vista per a redireccionar la navegació després de crear un projecte satisfactòriament. Malgrat l'error, realment sí s'ha creat el projecte, ho podem veure a home:

Per tant, anem a modificar la vista de la següent forma, per a definir un atribut anomenat success_url:

from django.urls import reverse_lazy
...
class ProyectoCreateView(CreateView):
    model = Proyecto
    fields = ['titulo', 'descripcion', 'fecha_creacion', 'year', 'categorias', 'imagen']
    success_url = reverse_lazy('home')

El nom 'home' ve del nom que li vam donar a la URL de la vista HomeView, creada en la unitat anterior.

Si creem ara un nou projecte, en salvar els canvis ens redireccionarà a la pàgina d'inici.

UpdateView

Anem en aquest apartat a implementar l'actualització del projecte. Per a això, utilitzem la vista genèrica UpdateView (la importem prèviament), i afegim una vista de la següent manera:

class ProyectoUpdateView(UpdateView):
    model = Proyecto
    fields = ['titulo', 'descripcion', 'fecha_creacion', 'year', 'categorias', 'imagen']
    success_url = reverse_lazy('home')

La URL serà la següent:

path('proyecto_update/<int:pk>/', views.ProyectoUpdateView.as_view(), name='proyecto_update'),

Provem la URL amb un ID de prova i veiem que recupera automàticament les dades del projecte amb aquest ID.

Cal notar que no hem especificat cap plantilla, ha seguit prenent la plantilla projecte_form.php que ja s'havia utilitzat per a la CreateView.

Hi ha una cosa en el formulari que no ens acaba de agradar, i és el text del botó. Anem a modificar la plantilla perquè, en funció de que estiguem en mode creació o edició, mostri "Crear" o "Actualizar", perquè sigui més orientatiu per a l'usuari. Això ho podem aconseguir comprovant si existeix una variable "object" en el context de la plantilla, passat per UpdateView. Així, modificaríem el codi de la plantilla lleugerament perquè el botó que dispara el formulari quedi:

<input type="submit" value="{{object|yesno:'Actualizar,Crear'}}">

De la mateixa forma, modifiquem l'element h2:

<h2>{{object|yesno:'Actualizar projecte,Creació d'un nou projecte'}}</h2>

Hem utilitzat l'operador ternari yesno en la plantilla.

Anem a modificar la pantalla d'inici perquè, en prémer en un dels projectes, vagi al formulari d'edició. A la següent unitat configurarem l'aplicació perquè només es pugui editar un projecte si l'usuari té privilegis per a això.

L'únic que hem de fer és canviar lleugerament la línia 12 de home.html per canviar el nom de la URL a la qual apunta l'enllaç de cada projecte:

<a href="{% url 'proyecto_update' pk=proyecto.id %}" class="p-5">

Provem que ara accedim al formulari d'edició després de prémer sobre un dels projectes.

DeleteView

Ara anem a implementar una vista que ens permeti esborrar un projecte determinat. Per a això anem a utilitzar la vista genèrica DeleteView. L'operació es durà a terme en dos passos:

1. En el formulari d'edició del projecte tindrem un botó Eliminar, només habilitat si el formulari està en mode edició, no creació.

2. Navegació a una pantalla de confirmació, on confirmarem que realment volem esborrar el registre.

Anem a això. Configurem la vista d'eliminació de la següent manera:

class ProyectoDeleteView(DeleteView):
    model = Proyecto
    success_url = reverse_lazy('home')

No és necessari especificar l'atribut template_name, ja que el nom de la plantilla segueix la nomenclatura per defecte (proyecto_confirm_delete.html, definida més avall).

I la seva corresponent URL:

path('proyecto_delete/<int:pk>/', views.ProyectoDeleteView.as_view(), name='proyecto_delete'),

La plantilla, amb nom "proyecto_confirm_delete.html", serà la següent:

{% extends 'portfolio/base.html' %}

{% block content %}
<div class="container">
    <h2>Eliminar un projecte</h2>
    <form method="post">{% csrf_token %}
        <p>Està segur que vol eliminar aquest projecte "{{ object }}"?</p>
        {{ form }}
        <input type="submit" value="Confirmar" class="btn btn-danger">
    </form>
</div>
{% endblock %}

Queda per crear el botó Eliminar en el formulari del projecte, i només s'habilitarà quan estigui en mode edició (el projecte ja ha estat creat). Com sabem si el formulari està en mode creació o edició? Molt fàcil: la vista UpdateView passa a la plantilla, com a part del context, un objecte "object" amb tots els atributs del projecte. Així, només hem de condicionar la visualització del botó Eliminar quan object no estigui buit. D'aquesta forma, afegim el següent a projecte_form.html, després de l'element form:

{% if object %}
<a class="btn btn-danger" href="{% url 'proyecto_delete' object.id %}"> Eliminar </a>
{% endif %}

Ja tenim el botó Eliminar en el formulari d'edició. Si premem, ens portarà a la següent pàgina:

No ens apareix correctament el nom del projecte, perquè no hem configurat la seva representació textual mitjançant la funció str en la configuració del model Proyecto.

ACTIVITAT: configura str en Proyecto, com vas fer en les activitats de la unitat anterior.

Després de confirmar l'eliminació, naveguem automàticament a la pàgina d'inici i comprovem que s'ha eliminat el projecte.

Redireccions

Ara tenim una forma de crear, actualitzar i eliminar projectes, però cada vegada que realitzem una acció, ens redirecciona a la pantalla de benvinguda. Volem canviar això de manera que, quan realitzem una operació, es redireccioni automàticament a una URL que tingui sentit. Podríem dissenyar les següents accions perquè la navegació sigui el més lògica possible:

  • Anar a pantalla d'edició després de la creació d'un projecte.
  • Anar a la pàgina de benvinguda després de l'edició exitosa d'un projecte.
  • Anar a la pàgina de benvinguda després de l'eliminació d'un projecte.

Les dues últimes les tenim resoltes, falta per solucionar la primera.

Tenim diferents formes de fer el mateix:

  • Mitjançant el mètode get_success_url en la vista, amb el qual podem construir dinàmicament la URL a la qual redireccionar, en funció de cada \"id\" de projecte.
  • Mitjançant el mètode get_absolute_url en el model, que s'utilitzaria per redireccionar després de crear o actualitzar un objecte d'aquest model.

Com només volem modificar el comportament de la creació, substituïm la vista ProyectoCreateView per la següent versió:

class ProyectoCreateView(CreateView):
    model = Proyecto
    fields = ['titulo', 'descripcion', 'fecha_creacion', 'year', 'categorias', 'imagen']

    def get_success_url(self):
        object = self.object
        return reverse_lazy('proyecto_update', kwargs={'pk': object.id})

Cal fer notar que ja disposem de l'objecte creat (a la BBDD) mitjançant self.object per a quan s'executa la funció get_success_url. Això ens permet una gran varietat de possibilitats, ja que podríem redireccionar la navegació de multitud de formes (a diferents vistes, amb diferents paràmetres) depenent de les característiques de l'objecte creat, o d'altres factors. Podríem, per exemple, redireccionar la navegació a la pàgina d'inici i passar un paràmetre per filtrar tots els projectes de l'any que acabem de crear. Ho deixarem així de moment.

Mixins

Els mixins són una forma de reutilització de codi en Django, de manera que podem definir una vista que hereti d'una altra vista genèrica, i aportarle funcionalitats extra mitjançant mixins.

Messages framework

Anem a utilitzar com a exemple el mixin SuccessMessageMixin, que ens va a permetre mostrar missatges de confirmació després de realitzar qualsevol acció. La documentació sobre això la pots trobar en aquest enllaç.

En primer lloc, la documentació ens diu que, per defecte, la configuració per a aquesta funcionalitat ja està preestablerta en settings.py. Anem a la part que ens interessa, que és la de agregar missatges en CBV. En views.py importem la classe SuccessMessageMixin com ens indica en l'exemple del enllaç, i modifiquem la vista ProyectoCreateView perquè quedi de la següent manera:

from django.contrib.messages.views import SuccessMessageMixin
...
class ProyectoCreateView(SuccessMessageMixin, CreateView):
    model = Proyecto
    fields = ['titulo', 'descripcion', 'fecha_creacion', 'year', 'categorias', 'imagen']
    success_message = "Projecte creat satisfactòriament"

    def get_success_url(self):
        object = self.object
        return reverse_lazy('proyecto_update', kwargs={'pk': object.id}) 

NOTA: és important situar els mixins a l'esquerra de la vista base. Els atributs i mètodes es resolen d'esquerra a dreta. En cas de que les classes de les que s'hereda tinguin un mateix nom d'un atribut i/o mètode, el de més a l'esquerra s'agafaria abans que el de la classe de més a la dreta, segons la llista especificada. És per això que la classe base es situa més a la dreta.

Està tot llest ja. Només ens queda poder mostrar per pantalla els missatges generats, com es mostra en aquest apartat de la documentació. Per a això, podem reservar un espai en la plantilla base de l'aplicació, que contingui el següent bloc (abans del bloc "content"):

{% block messages %}
    {% if messages %}
        {% for message in messages %}
        <div {% if message.tags %} class="alert alert-{{ message.tags }} alert-dismissible fade show" {%endif %} role="alert">
            {{ message }}
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
        </div>
        {% endfor %}
    {% endif %}
{% endblock messages %}

En crear un nou projecte, se'ns mostrarà el següent missatge:

És important notar que els missatges es mostraran en finalitzar l'acció que hem realitzat, però quan ja hàgim carregat la següent pàgina. Per tant, la navegació ha de realitzar-se de manera que tingui sentit per a l'usuari el missatge obtingut, i la pàgina actual en la que ens troben.

ACTIVITAT: Afegeix aquesta funcionalitat a la vista ProyectoUpdateView.

En aquests moments el missatge mostrat és estàtic, sempre es mostraria el mateix. Però podríem fer que es mostrés un missatge diferent, per exemple, en funció d'algun dels atributs del projecte (el seu nom, la seva representació textual, etc). Per a poder fer això, n'hi ha prou amb sobreescriure el mètode get_success_message de la classe (CreateView o UpdateView). Un exemple pot ser el següent, que fa que es mostri un missatge amb un text fix, combinat amb la representació textual del projecte:

def get_success_message(self, cleaned_data):
    return "Projecte '{}' actualitzat satisfactòriament".format(str(self.object))

En aquest enllaç tens una explicació més detallada de què fa format.

Com a apunt final, anem a fer un petit canvi en la configuració de settings.py. Simplement afegeix aquesta importació:

from django.contrib.messages import constants as messages
I afegeix aquesta línia al final:
MESSAGE_TAGS = {messages.ERROR: 'danger'}
Més endavant en aquest document s'explicarà el per què d'aquest canvi.

Mixins personalitzats

La possibilitat d'utilitzar mixins suposa un reaprofitament importantíssim de codi. Podem crear petites càpsules de codi identificades amb un nom, i anar dotant a les nostres classes de característiques addicionals. Al llegir la línia inicial d'una classe (els seus mixins i classe base de la qual hereta) hauríem de poder saber quin va a ser el propòsit de la classe.

En aquest apartat anem a crear un mixin nou. Per a això, primer creem un fitxer mixins.py en l'app portfolioapp:

Crear un mixin és crear una classe nova. Per a crear una classe des de zero, heredem de la classe "object", encara que podem partir d'altres classes.

Observant les nostres vistes, què podríem portar-nos a un mixin? potser les diferents opcions són argumentables, però amb el codi que hem generat fins ara, es repeteixen alguns atributs de les classes ProyectoCreateView i ProyectoUpdateView, i l'herència de SuccessMessageMixin. Anem per tant a crear un mixin que agrupi tot això, i a més a redireccionar les dues accions (creació i actualització) a la pantalla d'actualització. En mixins.py:

from django.contrib.messages.views import SuccessMessageMixin
from .models import Proyecto

class ProyectoMixin(SuccessMessageMixin):
    model = Proyecto
    fields = ['titulo', 'descripcion', 'fecha_creacion', 'year', 'categorias', 'imagen']
    def get_success_url(self):
        object = self.object
        return reverse_lazy('proyecto_update', kwargs={'pk': object.id})

En views.py modifiquem les vistes perquè quedin de la manera següent:

from .mixins import ProyectoMixin
...
class ProyectoCreateView(ProyectoMixin, CreateView):
    success_message = "Projecte creat satisfactòriament"

class ProyectoUpdateView(ProyectoMixin, UpdateView):
    success_message = "Projecte actualitzat satisfactòriament"

Si provem l'aplicació, hauria de funcionar igual que abans, amb els avantatges que el nostre codi és molt més modular, i si haguérem de canviar alguna cosa del comportament comú a aquestes dues vistes, només caldria modificar-ho en un únic lloc.

NOTA: Un mixin hauria de representar una agrupació de funcionalitats que pogueren ser reutilitzades en moltes vistes, fins i tot de diferents apps. En aquest cas, estem portant al mixin l'atribut model junt amb altres atributs i mètodes que el particularitzen per a vistes basades en el model Projecte. Per tant, no podrem utilitzar aquest mixin en altres vistes d'altres models. Podríem acabar fins i tot portant tota la lògica de les dues vistes al mixin, però es difuminarà el propòsit del mixin, i les vistes es reduirien a un mínim que no tindria sentit. La pràctica ens dirà quan portem determinats comportaments a un mixin, i quan els deixem en una vista determinada.

Personalització de vistes genèriques

Ja hem vist algun exemple de com modificar el comportament d'una vista, en concret el mètode get_success_url (de CreateView i UpdateView). En aquest apartat anirem una mica més enllà i descobrirem quines opcions tenim disponibles per personalitzar, i farem alguns exemples.

Prenem com a exemple la vista CreateView. El primer que ens hem de preguntar és quins mètodes podem personalitzar. Anem a la documentació d'aquesta vista, i ens dóna una llista de les seues característiques:

Atributs:

Entre els atributs, la documentació especifica un anomenat "object". Si l'objecte encara no ha estat creat, aquest atribut tindrà el valor None, i si l'objecte ja ha estat creat contindrà una còpia. Això ho hem utilitzat en el mètode get_success_url, per passar-li a la vista "projecte_update" l'id de l'objecte que s'ha creat en prémer el botó "Crear".

Ancestres:

Aquesta és la llista d'ancestres:

NOTA: En aquest apartat s'especifiquen les sigles MRO (Method Resolution Order), que és l'algoritme que utilitza Python per a la resolució d'un mètode/atribut en la jerarquia de classes, quan s'utilitza l'herència múltiple. Aquest punt és el que s'ha discutit en l'apartat de mixins.

Si consultem cadascuna d'aquestes classes, podem veure els atributs i mètodes de cadascuna d'elles. Per exemple:

  • Mitjançant SingleObjectTemplateResponseMixin podem recuperar la plantilla per a una vista que opere sobre un sol objecte. Entre altres, podem veure que aquest mixin té un mètode get_template_names, que estableix una llista de plantilles candidates a ser utilitzades. Ens diu que es busca una plantilla amb la forma /.html. Aquesta estructura és coherent amb el nom de la plantilla que utilitzàvem ProjecteCreateView, anomenada projecte_form.html, dins de l'app portfolioapp. Per tant hem resolt el misteri de com era possible que Django trobés la plantilla del nostre projecte automàticament. Podríem, per exemple, sobreescriure aquest mètode per obtenir el nom d'una plantilla per a la nostra vista, segons un altre tipus de lògica.
  • En ModelFormMixin podem veure que estan quasi tots els atributs que hem utilitzat fins ara, i el mètode get_success_url que ja havíem sobreescrit. Tenim a més altres mètodes interessants, com form_valid (que ens permetrà realitzar determinades accions una vegada s'ha verificat que els camps del formulari són vàlids, abans de crear l'objecte i redireccionar).

En els següents subapartats farem algunes personalitzacions a algunes de les vistes que venim utilitzant.

form_valid

Imaginem que volem enviar una notificació als subscriptors del nostre lloc web (si tinguérem la capacitat de portar la seua alta i gestió). Volem notificar-los que hem publicat un nou projecte, no volem que es perden res. Això és molt comú en qualsevol tipus d'aplicació, no podem pretendre que els usuaris estiguen constantment refrescant una pàgina web per saber si hi ha novetats, hem d'enviar notificacions (normalment en forma d'e-mail).

Per això, en qualsevol projecte d'una certa envergadura, disposarem d'una part de l'aplicació que s'encarregue de registrar tots aquells esdeveniments susceptibles de ser notificats, així com la lògica per trobar els destinataris, conformar els missatges, i enviar els e-mails (ja directament, o a través d'un servei com mailchimp).

Bé, el primer pas és poder registrar l'esdeveniment de creació d'un projecte nou. Però on el registrem? Ho podem fer en una taula nova. Aquesta taula... la creem en l'app "portfolioapp" que tenim actualment en el projecte? Doncs no és el més adequat: començarem a desenvolupar lògica per a una nova funcionalitat sobre notificacions. Té molt més sentit crear una app nova en el projecte, que registre la base de tot això, i a més la puguem reutilitzar en futurs projectes.

Per tant, creem una nova app anomenada "notificacions" (mitjançant l'ordre startapp), l'afegim a INSTALLED_APPS de settings.py, i dins del seu fitxer models.py definim un nou model. Una versió molt molt molt simplificada seria:

class NotificaProjecte(models.Model):
    projecte = models.ForeignKey(Projecte, on_delete=models.PROTECT)
    data = models.DateTimeField(default=now)
    notificat = models.BooleanField(default=False)

NOTA: En un projecte de més envergadura, si volguérem notificar esdeveniments realitzats sobre diferents tipus de models, hauríem de tenir un model NotificaXXX per cada tipus d'objecte a modificar? El problema és que la clau forana "projecte" va enllaçada amb el model Projecte, però no pot anar enllaçada amb cap altre model. Ens estalviaria molt d'esforç el poder enllaçar aquest atribut a qualsevol model sobre el qual volguérem notificar algun esdeveniment. Com es podria fer en Django? Molt fàcil: mitjançant el que s'anomena el contenttype framework de Django, mitjançant el qual podem establir claus foranes genèriques, a qualsevol altre model. Un mateix model ens pot servir de detall per a molts altres.

Després de crear el model, fem les migracions i migrem, i configurem l'administrador per a aquest nou model.

El nostre objectiu és que cada vegada que es cree un projecte nou es cree, alhora, un nou objecte per a la classe NotificaProjecte.

Bé, sobreescriurem el mètode form_valid de la vista ProjecteCreateView per realitzar dues accions:

  • Comprovar que la data de creació del projecte no és anterior al moment actual (es tracta només d'un exercici il·lustratiu).
  • Creació tant del projecte com de la seua notificació corresponent.

Primer importem el següent en views.py:

from django.contrib import messages
from django.db import transaction
from django.utils import timezone

Tot això és tan fàcil com afegir el següent mètode a la classe ProjecteCreateView:

def form_valid(self, form):
    if form.instance.data_creacio < timezone.now():
        messages.error(self.request, "La data/hora del projecte no pot ser anterior a l'actual.")
        return super(ProjecteCreateView, self).form_invalid(form)
    else:
         projecte = form.save()
         NotificaProjecte.objects.create(projecte=projecte)
         return super(ProjecteCreateView, self).form_valid(form)

En el moment en què s'executa form_valid, l'objecte encara no està creat realment en la base de dades, o almenys no s'ha fet el commit final. És per això que l'objecte està disponible en form.instance, encara que podem revertir els canvis cridant al mètode form_invalid de la superclasse.

Anem a provar-ho. Primer intentem crear un projecte amb una data anterior a l'actual, i obtenim el següent missatge d'error:

Fenomenal. Anem a provar la resta de la lògica. Modifiquem la data i guardem:

Bé, anem a veure si s'ha creat un altre registre de notificació, mitjançant l'administrador:

Perfecte! Hem aconseguit crear un registre en una altra taula, al mateix temps que hem creat un projecte. Ara tindrem el següent problema: en intentar esborrar un projecte, obtindrem un error avisant-nos que tenim registres dependents en la BBDD. Això ho tractarem en les activitats.

MOLT IMPORTANT: Cal fer notar el següent:

  • Realment no hem sobreescrit el mètode form_valid, l'hem estès. Hem realitzat una sèrie d'operacions i al final hem cridat al mètode form_valid o form_invalid de la superclasse. És a dir, només hem afegit alguns passos previs a la lògica heretada. La nomenclatura per cridar al mètode de la superclasse és mitjançant super.
  • Hem utilitzat transaccions de Django per assegurar-nos que la creació del projecte i la seua notificació es realitza de manera conjunta, i si es produeix algun tipus d'error en un d'ells, que no es realitze la transacció en la base de dades.

NOTA: Existeix una altra aproximació per realitzar aquest tipus d'operacions en Django, que són els senyals o signals. Aquest mètode suposa que es disparen determinades accions en la BBDD quan es detecten determinats esdeveniments. Hi ha ocasions en què això ens permet controlar molt millor el moment i les accions que volem sincronitzar amb un determinat esdeveniment en el sistema, i no hauríem de preocupar-nos de replicar la mateixa lògica en totes aquelles parts del codi que ho necessitem. Serien similars als triggers de bases de dades.

Tags de missatges

Django ha sabut posar-li fins i tot el color correcte al missatge d'error... o se'ns escapa alguna cosa? Revisem la plantilla base.html, veiem que el color ve donat per la part que diu:

alert-{{message.tags}}

Anem a veure-ho en l'exemple del missatge d'error de la comprovació de dates:

Genial, tenim la classe alert-danger, que és la que ens dóna el color roig.

Bé, la classe bootstrap que estem assignant a eixe bloc ve donada pel mateix codi del missatge d'error. En les alertes de bootstrap veiem que la classe per a missatges d'error és "alert-danger".

Però: en la documentació del framework de missatges de Django, els codis són els següents:

I segons bootstrap, les classes de les alertes que es podrien correspondre amb els nostres codis de missatges són:

alert-info

alert-success

alert-warning

alert-danger

Veiem que concatenant "alert-" amb el codi de cada tipus de missatge en Django podem conformar la classe adequada, per això utilitzem alert-{{message.tags}}.

T'has adonat ja d'on està la discrepància? Sí: el codi dels missatges d'error en Django és "error", no "danger". Així que, com pot estar derivant la classe correctament? Bé, això és per la modificació que vam fer anteriorment en settings.py:

MESSAGE_TAGS = {messages.ERROR: 'danger'}

Gràcies a això, estem utilitzant "danger" i no "error" (el valor per defecte de Django) com a etiqueta d'error. Ho podem comprovar fàcilment: comentem eixa línia en settings.py i intentem forçar l'error:

Ara veiem que ha format la classe "alert-error". Com que eixa classe no la reconeix Bootstrap, provoca que el missatge no es visualitze correctament.

FINALMENT, descomentem la línia de MESSAGE_TAGS en settings.py, per deixar eixa modificació.

Formularis

Les vistes CreateView i UpdateView han creat automàticament un formulari basant-se en els camps que hem especificat en l'atribut fields, però l'aspecte i distribució d'eixe formulari és clarament millorable.

Els formularis són una peça clau en la interacció amb l'usuari, per tant cal prestar especial atenció a aquests elements en la nostra aplicació. La disposició i agrupació dels camps, les utilitats que ajuden l'usuari a decidir els valors introduïts, els missatges (informatius, d'avís, error, i èxit) i el comportament de l'aplicació després de l'enviament de les dades i el seu processament, han d'estar especialment analitzats i depurats abans de llançar la nostra aplicació. Conceptes de disseny d'interfícies com la usabilitat, estan estretament relacionats amb els formularis.

Django ens proporciona una sèrie d'utilitats per agilitzar la creació i configuració de formularis. Principalment, disposarem de dues classes que realitzaran moltes de les tasques bàsiques per nosaltres:

  • Classe Form: Representa la forma més bàsica de crear un formulari, permetrà tenir més control. La utilitzarem normalment per a formularis que no gestionen directament un model (operacions bàsiques de creació, actualització, eliminació), com per exemple el formulari de login, o un simple formulari de contacte que envie un e-mail (sense manipular dades de la BBDD). Les classes basades en la classe Form s'utilitzen normalment en una FormView, de manera que realitze la resta de la lògica amb les dades recollides pel formulari.
  • Classe ModelForm: Aquesta classe serà la que utilitzarem majoritàriament, per realitzar la gestió dels nostres models, i la utilitzarem per construir classes que més tard utilitzarem en una CreateView, una UpdateView i/o DeleteView.

En aquest enllaç trobaràs exemples d'utilització dels següents parells:

  • Classe Form, amb classe FormView.
  • Classe ModelForm, amb CreateView, UpdateView i DeleteView.

En el següent subapartat crearem un formulari per al manteniment dels projectes, utilitzant la classe ModelForm. En la pròxima unitat utilitzarem la classe Form per realitzar un formulari per al login de l'aplicació.

Gestió de projectes amb ModelForm

Creació de la classe ProjecteForm

El primer pas serà la creació d'un fitxer forms.py en l'app de portfolioapp (típicament tindrem un fitxer forms.py dins de cada app), i incloem el següent:

from django import forms
from .models import Projecte
class ProjecteForm(forms.ModelForm):
    class Meta:
        model = Projecte
        fields = ['titol', 'descripcio', 'data_creacio', 'any', 'categories', 'imatge']

Per poder utilitzar-lo en les vistes, importem aquesta classe en views.py. A continuació inclourem la següent línia en les classes ProjecteCreateView i ProjecteUpdateView:

form_class = ProjecteForm
Hem d'eliminar l'atribut "fields" que teníem en el mixin ProjecteMixin, ja que si no ho fem l'aplicació ens tornarà un error.

Després de fer els canvis, consultem el formulari, i no apreciem cap diferència:

Ara ens hauríem de preguntar: com és possible que en el camp Categoria aparega un multiselector si no ho hem especificat en cap lloc? Django ha decidit utilitzar eixe "widget" basant-se en la definició del model. Existeixen altres tipus de widgets per defecte que podríem utilitzar en els nostres formularis? Sí, tens una llista completa en aquest enllaç.

Podríem, a més, utilitzar widgets de tercers en els nostres formularis, com per exemple els desenvolupats en aquesta app anomenada django-filter, que ens permeten tenir un ventall de selectors més rics per als nostres formularis.

Layout del formulari

Volem que el nostre formulari tinga una aparença més atractiva i siga més funcional. Volem que mostre uns missatges d'error associats a cadascun dels camps, no els preestablits en el navegador.

Podem establir diversos graus de control sobre el formulari, però en aquest apartat manipularem el layout (disposició) dels camps i establirem un major grau de control sobre ells. D'aquesta manera, s'ha desenvolupat una nova versió de la plantilla que s'adjunta al present document, anomenada projecte_form.html. A continuació veiem el resultat:

Ha canviat un poc la disposició dels camps, però tampoc és gran cosa.

L'aspecte és clarament millorable, com podem aconseguir un formulari més adaptat als actuals estàndards d'interfícies d'usuari? Més avall en aquest document ho farem mitjançant un paquet addicional anomenat Django Crispy Forms.

Mètode clean

En un apartat anterior vam implementar en la vista ProjecteCreateView la restricció que la data de creació no fóra anterior al moment actual. El mateix ho podem fer com a part del formulari, mitjançant els mètodes clean. Tenim dos tipus:

  • Mètode clean comú a tots els camps: amb aquest mètode podem comprovar dependències entre els camps o tenir en compte consideracions addicionals.
  • Mètodes clean propis de cada camp: es defineixen com clean_[nom de l'atribut], i són específics per a cada camp del formulari.

Anem a implementar un mètode clean per al camp data_creacio. En el formulari ProjecteForm (a l'altura de la classe Meta, no dins), introduïm el següent:

def clean_data_creacio(self):
    data_creacio = self.cleaned_data['data_creacio']
    if data_creacio < timezone.now():
        raise ValidationError("La data/hora del projecte no pot ser anterior a l'actual.")
    return data_creacio

Comprovem el missatge d'error, que és clarament millorable estèticament:

Les validacions del formulari es dispararan abans que les de la vista, encara que podem comentar la validació de les vistes per no duplicar codi.

Això ho millorarem amb Django Crispy Forms.

Podem realitzar aquesta validació on més ens interesse: si la realitzem en el formulari, qualsevol vista que l'utilitze conservarà aquesta validació; si la realitzem en les vistes, hauríem de duplicar la lògica o crear un mixin.

Function based views

En aquest document hem utilitzat CBV en tots els exemples, però podríem haver-los realitzat amb FBV. Normalment utilitzar aquesta segona aproximació ens dóna més control sobre el que estem fent, però per a gran part dels casos no ho necessitem. En general, utilitzarem FBV quan necessitem alguna cosa especialitzada i concreta, per a tot el demés ens beneficiarem de totes les funcionalitats que ja venen implementades en les classes.

Ara que hem revisat l'apartat de formularis de Django, podem abordar les FBV, ja que si utilitzem aquest mètode, hem de definir els formularis prèviament (amb CBV ens venen els formularis fets, si volem els que venen per defecte).

A mode il·lustratiu, revisarem aquest enllaç per establir els paral·lelismes entre les dues aproximacions.

Interactivitat

El desenvolupament en costat servidor fa moltíssimes coses per nosaltres, representa en la majoria de les aplicacions una bona part del total, però hi ha moltíssimes més coses que atendre. La interacció amb l'usuari és un altre factor essencial, i funcionalitats desenvolupades en el costat servidor que no són utilitzades de manera efectiva per l'usuari (per raons d'usabilitat o altres) són funcionalitats que no existeixen en la pràctica. Per tant, cal cuidar i mimar especialment la percepció que l'usuari té de la nostra aplicació.

És per això que la part servidor s'ha de complementar amb la part client (entre altres), que ocorre en el navegador web, per al cas de les aplicacions web.

Existeixen multitud de tecnologies, llibreries, tècniques, que ens permeten ajustar de manera molt més fina la interacció amb l'usuari. Amb el model MVC podem realitzar moltíssimes adaptacions, però el model que ens permet major versatilitat és el basat en serveis REST, que ens permet separar completament la part servidor i la part client, cedint a aquest últim tot el protagonisme en la interacció amb l'usuari.

En els següents subapartats veurem uns exemples de com podem millorar l'aspecte i interacció de la nostra aplicació amb diferents paquets i llibreries.

Al voltant de Django es desenvolupen multitud de paquets que poden facilitar moltes d'aquestes tasques. En aquest enllaç trobaràs molts d'ells. Podem destacar:

I molts més.

Django Crispy Forms

Anem a continuar amb el treball iniciat en l'apartat anterior. Anem a reprendre el formulari que no tenia un aspecte molt usable, i l'anem a millorar amb Django Crispy Forms. Hem d'instal·lar 2 paquets:

Instal·la els dos paquets que s'indiquen mitjançant pip (en el nostre entorn virtual), congela requirements.txt, i modifica INSTALLED_APPS en settings.py. Segueix les instruccions d'instal·lació dels dos enllaços. A més, assegura't d'incloure les següents variables en settings.py (per al segon paquet instal·lat):

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"

Anem a la plantilla de projecte_form.html, i revertim els canvis que havíem fet. La deixem de la següent manera:

{% extends 'portfolio/base.html' %}
{% load crispy_forms_tags %}

{% block content %}
<div class="container">
    <h2>{{object|yesno:'Actualitzar projecte,Creació d'un nou projecte'}} </h2>
    <form method="post" enctype="multipart/form-data" novalidate>
        {% crispy form %}
        <input type="submit" class="btn btn-success mr-2" value="{{object|yesno:'Actualitzar,Crear'}}" >
        {% if object %}
        <a class="btn btn-danger" role="button" href="{% url 'projecte_delete' object.id %}"> Eliminar </a>
        {% endif %}
    </form>
</div>
{% endblock %}

Anem a fer màgia amb Django Crispy Forms i afegim la següent línia després de l'extends:

{% load crispy_forms_tags %}
La línia que diu {{ form.as_p }} la substituïm per:
{% crispy form %}
Finalment, esborrem la línia {% csrf_token %} perquè Crispy Forms ja ho inclou per nosaltres, i incloem "novalidate" a l'element form, d'aquesta manera anul·lem les validacions natives del navegador.

Comprovem ara el formulari i veiem una millora claríssima:

Abans de continuar, anem a veure què ens ofereix aquest paquet per canviar el nostre formulari. En la documentació es detallen dues classes:

  • FormHelper: defineix el comportament en la representació del formulari.
  • Layout: com el seu nom indica, defineix l'estructura dels elements del formulari.

Primer introduirem el següent codi en la classe ProjecteForm:

    def __init__(self, *args, **kwargs):
        super(ProjecteForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper(self)
        self.helper.form_tag = False # No incloure <form></form>

        self.helper.layout = Layout(
            Div(
                Field('titol'),
                Field('descripcio'),
            ),
            HTML('<hr>'),
            Div(
                Div(Field('data_creacio'), css_class="col-6"),
                Div(Field('any'), css_class="col-6"),
                css_class="row"
            ),
            HTML('<hr>'),
            Div(
                Field('categories', css_class="mb-3"),
                Field('imatge'),
                css_class="mb-5"
            ),
        )

Observacions:

  • S'estén el constructor de la classe ProjecteForm per acomodar els canvis amb Crispy Forms.
  • S'estableix l'atribut form_tag del helper a False perquè no s'incloga l'element
    en l'HTML, ja que ja el tenim establert en projecte_form.html.
  • Utilitzem elements com Layout, Div, HTML per estructurar els elements, i els donem estil amb l'atribut css_class.

D'aquesta manera podríem modificar l'estructura del formulari de manera dinàmica, mitjançant Python.

Anem a veure com ha quedat el formulari: