Amb Flask vas muntar cada peça de la web de Papyrus a mà: vas triar on desar el catàleg (el JSON de M6), vas escriure la validació del POST camp a camp, i si demà volguessis un panell perquè la Marta editi llibres sense tocar codi, l'hauries de construir. Django és la resposta de l'altra escola: un framework amb bateries incloses que porta de sèrie persistència, panell d'administració, formularis validats, gestió d'usuaris i més. En aquesta lliçó arrencaràs un projecte Django des de zero, entendràs la seva arquitectura MTV i la seva estructura de fitxers (que imposa, no suggereix), redefiniràs el Llibre de M5 com un model amb persistència regalada i acabaràs editant el catàleg des de l'admin — sense haver escrit ni una línia d'HTML. El mateix Papyrus, l'altre camí.

Contingut

  1. Filosofia: bateries incloses (quan Django, quan Flask)
  2. Instal·lació i startproject: neix papyrus_web
  3. Projecte vs app: startapp cataleg
  4. L'estructura de fitxers, fitxer a fitxer
  5. El patró MTV (i el seu cosí MVC)
  6. Primera vista: HttpResponse i urls.py
  7. El model Llibre: de dataclass a models.Model
  8. Migracions: makemigrations i migrate
  9. L'admin de Django: el regal estrella
  10. runserver i el mapa complet

Filosofia: bateries incloses (quan Django, quan Flask)

Flask et dona un nucli mínim i llibertat total; Django et dona un electrodomèstic complet i les seves instruccions. Cap dels dos no és "millor": resolen contextos diferents.

Criteri Flask Django
Persistència La tries tu (el nostre JSON de M6) ORM inclòs: models que es desen sols
Panell d'administració El construeixes tu Inclòs i automàtic
Formularis validats A mà (el POST de 10-03) ModelForm inclòs (10-05)
Usuaris i sessions Extensions de tercers Inclosos
Estructura de projecte La decideixes tu La imposa Django (i això és bo en equip)
Ideal per a APIs, projectes petits, aprendre peça a peça Aplicacions completes amb parts estàndard
Risc típic Reinventar rodes Carregar amb peces que no fas servir

Regla pràctica: si el teu projecte és sobretot una API o una cosa petita i peculiar, Flask; si és "una aplicació web clàssica" — catàleg, altes, usuaris, administració —, Django t'estalvia setmanes. Papyrus, que és exactament això, és un cas de llibre (mai tan ben dit).

Instal·lació i startproject: neix papyrus_web

En un entorn virtual nou (M1: un venv per projecte):

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install django
django-admin startproject papyrus_web
cd papyrus_web

pip install django ha instal·lat també l'ordre django-admin, el generador de projectes. startproject papyrus_web crea una carpeta amb l'esquelet complet — primera diferència cultural amb Flask, on vas començar amb un app.py buit: Django et dona l'estructura feta perquè la seva filosofia és que tots els projectes Django s'assemblin.

Projecte vs app: startapp cataleg

Django distingeix dos conceptes que convé fixar aviat:

  • El projecte (papyrus_web) és el conjunt: configuració global, URLs arrel, el lloc sencer.
  • Una app és un component funcional amb els seus models, vistes i plantilles: el catàleg, algun dia les reserves, potser els socis. Un projecte conté diverses apps; una app ben feta es podria reutilitzar en un altre projecte.

Creem la primera:

python manage.py startapp cataleg

I la registrem al projecte, a papyrus_web/settings.py, perquè Django la tingui en compte:

INSTALLED_APPS = [
    "django.contrib.admin",          # el panell d'administracio
    "django.contrib.auth",           # usuaris i permisos
    # ... altres bateries incloses ...
    "cataleg",                       # ← la nostra app
]

Aquest INSTALLED_APPS és literalment la llista de bateries connectades: fixa't en quantes coses vénen enceses de fàbrica.

L'estructura de fitxers, fitxer a fitxer

Després de les dues ordres, l'arbre és aquest (els fitxers importants, comentats):

papyrus_web/                  ← carpeta arrel del projecte
├── manage.py                 ← la teva navalla suissa: runserver, migrate, startapp...
├── papyrus_web/              ← el "cor" del projecte (configuracio)
│   ├── settings.py           ← configuracio global
│   ├── urls.py               ← taula d'URLs arrel
│   └── wsgi.py / asgi.py     ← punts d'entrada per a servidors (produccio)
└── cataleg/                  ← l'app
    ├── models.py             ← les dades: aqui renaixera Llibre
    ├── views.py              ← les vistes (com les de Flask)
    ├── admin.py              ← que s'administra des del panell
    ├── migrations/           ← historial de canvis dels models
    └── tests.py              ← Django tambe creu en els tests (M9 somriu)
Fitxer Responsabilitat Equivalent Flask
manage.py Executar ordres del projecte el CLI flask
settings.py Configuració: apps, base de dades, idioma, zona horària el poc que configuraves a app.py
urls.py Mapejar URLs → vistes els @app.route (aquí sense decorador!)
views.py Rebre petició, retornar resposta les teves funcions vista
models.py Definir les dades i la seva persistència el teu models.py + magatzem.py + JSON
admin.py Configurar el panell d'administració no existeix: l'hauries de fer tu

Dos ajustos de cortesia a settings.py abans de continuar: LANGUAGE_CODE = "ca" i TIME_ZONE = "Europe/Madrid" — l'admin et parlarà en català.

El patró MTV (i el seu cosí MVC)

Django organitza el codi segons el patró MTV: Model–Template–View. És la separació de responsabilitats que venim practicant des de M3, amb noms propis:

flowchart LR
    P["Petició<br/>GET /llibres/"] --> U["urls.py<br/>quina vista atén això?"]
    U --> V["View (views.py)<br/>lògica: quines dades calen"]
    V --> M["Model (models.py)<br/>les dades: Llibre i la seva persistència"]
    M --> V
    V --> T["Template<br/>HTML amb forats"]
    T --> R["Resposta HTML"]
  • Model: les dades i les regles per accedir-hi. A Papyrus-Flask eren la dataclass més magatzem.py més el JSON; a Django serà una sola classe.
  • Template: la presentació — com les plantilles Jinja2 de 10-02, amb una sintaxi gairebé idèntica.
  • View: la lògica que connecta totes dues — rep la petició, consulta models, tria plantilla.

Si has sentit parlar d'MVC (Model-View-Controller), el patró clàssic: és el mateix amb les etiquetes mogudes. La "View" de Django fa de Controller de l'MVC, i el Template fa de "View" de l'MVC. Confús d'anomenar, idèntic d'esperit: dades, lògica i presentació, cadascú al seu lloc.

Primera vista: HttpResponse i urls.py

El "hola món" Django, per veure el mecanisme despullat. A cataleg/views.py:

from django.http import HttpResponse

def inici(request):
    return HttpResponse("<h1>Papyrus, llibreria de barri</h1>")

Dues diferències amb Flask salten a la vista: la funció rep sempre request com a primer paràmetre (a Flask era un objecte global que importaves), i no hi ha decorador de ruta. A Django, les URLs viuen separades de les vistes, a papyrus_web/urls.py:

from django.contrib import admin
from django.urls import path
from cataleg import views

urlpatterns = [
    path("admin/", admin.site.urls),   # el panell, ja endollat de serie
    path("", views.inici),             # "" = l'arrel del lloc
]

path(ruta, vista) fa el que feia @app.route: apuntar a la taula. La diferència és filosòfica: Django prefereix totes les URLs juntes en un índex en lloc de repartides per decoradors — en projectes grans aquest índex únic s'agraeix. Arrenca i comprova-ho:

python manage.py runserver

http://127.0.0.1:8000 (Django fa servir el port 8000; Flask, el 5000) mostra la teva salutació. runserver inclou recàrrega automàtica, com el mode debug de Flask — i amb la mateixa advertència: és un servidor de desenvolupament, no de producció.

El model Llibre: de dataclass a models.Model

Aquí arriba el moment estrella de la lliçó. Recorda la dataclass de M5:

@dataclass
class Llibre:
    titol: str
    preu: float
    estoc: int = 0

La seva reencarnació Django, a cataleg/models.py:

from django.db import models

class Llibre(models.Model):
    titol = models.CharField(max_length=200)
    preu = models.DecimalField(max_digits=6, decimal_places=2)
    estoc = models.IntegerField(default=0)

    IVA_LLIBRES = 0.04
    DESCOMPTE_SOCI = 0.05

    def preu_final(self, soci=False):
        preu = float(self.preu) * (1 + self.IVA_LLIBRES)
        if soci:
            preu *= 1 - self.DESCOMPTE_SOCI
        return round(preu, 2)

    def hi_ha_estoc(self):
        return self.estoc > 0

    def __str__(self):
        return self.titol

Compara membre a membre:

Dataclass (M5) Model Django Què hi afegeix Django
titol: str CharField(max_length=200) Longitud màxima, validació
preu: float DecimalField(max_digits=6, decimal_places=2) Decimals exactes per a diners (millora!)
estoc: int = 0 IntegerField(default=0) Tipus i valor per defecte persistits
hereta de models.Model Desar-se, carregar-se, cercar-se: gratis

Les mateixes tres dades de sempre, els mateixos mètodes preu_final() i hi_ha_estoc() (Hamlet continuarà costant 9.83 per a socis — els invariants de M9 viatgen amb nosaltres). La novetat és el que no veus: per heretar de models.Model (herència de M5 en acció), cada Llibre sap desar-se amb llibre.save() i consultar-se amb Llibre.objects... (l'espremerem a 10-05). A Flask vas escriure carregar_cataleg i desar_cataleg sobre JSON; això és la persistència que Django et regala. Per sota fa servir una base de dades SQLite — un fitxer db.sqlite3 al teu projecte, sense instal·lar ni configurar res. La teoria de bases de dades SQL queda fora d'aquest curs: avui en tenim prou que el regal funcioni. Un detall professional de propina: DecimalField evita els arrodoniments estranys del float amb diners — Django fins i tot millora la nostra dataclass.

Migracions: makemigrations i migrate

Definir el model encara no crea res: cal dir a la base de dades quina forma tenen les dades. Aquest diàleg són les migracions, en dos passos:

python manage.py makemigrations cataleg
# Migrations for 'cataleg':
#   cataleg/migrations/0001_initial.py
#     + Create model Llibre

python manage.py migrate
# Applying cataleg.0001_initial... OK  (i les d'admin, auth...)

Conceptualment:

  • makemigrations compara els teus models.py amb l'últim estat conegut i escriu la recepta del canvi (un fitxer Python a migrations/): "crear la taula de Llibre amb aquestes columnes".
  • migrate aplica les receptes pendents a la base de dades real.

És control de versions per a la forma de les teves dades: cada canvi futur del model (afegir un camp autor, per exemple) generarà la seva migració 0002_..., i qualsevol company reprodueix la teva base de dades executant migrate. Veuràs que el primer cop s'apliquen també migracions d'admin i auth: són les taules de les bateries incloses preparant-se.

L'admin de Django: el regal estrella

I ara, la demostració de força. Tres línies a cataleg/admin.py:

from django.contrib import admin
from .models import Llibre

admin.site.register(Llibre)

Un superusuari per entrar (et demanarà usuari i contrasenya per terminal):

python manage.py createsuperuser

Arrenca runserver, entra a http://127.0.0.1:8000/admin/, identifica't i contempla: un panell d'administració complet — llistat de llibres, formulari d'alta amb validació per tipus, edició, esborrament amb confirmació, cercador — sense haver escrit ni una sola plantilla ni una sola vista. Dona d'alta el catàleg canònic: L'Odissea (12.50, 4), Hamlet (9.95, 6), El Quixot (15.90, 8) i Faust (21.00, 10). La Marta ja pot gestionar l'inventari des del navegador; a Flask, aquest panell t'hauria costat unes quantes tardes. El __str__ que hem definit és el que l'admin mostra als llistats — sense ell veuries «Llibre object (1)».

runserver i el mapa complet

El cicle de treball Django que repetiràs mil vegades, resumit:

python manage.py runserver          # desenvolupar i provar
# ... edites models.py ...
python manage.py makemigrations     # anotar el canvi de dades
python manage.py migrate            # aplicar-lo

Errors Comuns i Consells

  • Oblidar registrar l'app a INSTALLED_APPS: el símptoma clàssic és makemigrations responent "No changes detected". Django només mira les apps registrades.
  • Editar models.py i no migrar: en accedir-hi veuràs OperationalError: no such table. Model canviat = makemigrations + migrate, sempre, en aquest ordre.
  • Confondre django-admin amb manage.py: django-admin només per a startproject; després, tot amb python manage.py ..., que carrega la configuració del teu projecte.
  • Fer servir FloatField per a diners: la temptació ve de la dataclass. DecimalField existeix precisament per a imports; acostuma-t'hi des del primer model.
  • Esborrar db.sqlite3 o migrations/ "per arreglar" un error: en aprenentatge t'ho pots permetre, però és dinamita: perds dades i historial. Millor entendre l'error — gairebé sempre és una migració sense aplicar.
  • Buscar l'if __name__ == "__main__": a Django no arrenques tu l'aplicació; l'arrenca manage.py. Deixa anar el control: el framework crida el teu codi, no a l'inrevés (això és, literalment, el que defineix un framework).

Exercicis

Exercici 1: el camp autor

El catàleg canònic sempre ha tingut autors (Homer, Shakespeare, Cervantes, Goethe) i el model no els desa. Afegeix autor = models.CharField(max_length=100, default="") al model, genera i aplica la migració, i completa els autors des de l'admin. Ajusta __str__ perquè mostri "Hamlet — Shakespeare".

Exercici 2: una pàgina de socis

Afegeix la vista socis (amb HttpResponse i HTML mínim llistant en Lluís, la Marta i en Pau amb els seus carnets LLUIS-001, MARTA-002 i PAU-003) i connecta-la a la URL /socis/. Comprova-la al navegador.

Exercici 3: preus de soci a la consola de Django

python manage.py shell obre un intèrpret amb el teu projecte carregat. Fes-lo servir per recórrer tots els llibres i imprimir titol: preu_final(soci=True). Verifica contra els valors canònics: 12.35, 9.83, 15.71 i 20.75. (Avançament inevitable: per a "tots els llibres" necessitaràs Llibre.objects.all() — la porta de 10-05.)

Solucions

Exercici 1:

class Llibre(models.Model):
    titol = models.CharField(max_length=200)
    autor = models.CharField(max_length=100, default="")
    # ... la resta igual ...

    def __str__(self):
        return f"{self.titol} — {self.autor}"
python manage.py makemigrations cataleg   # crea 0002_llibre_autor.py
python manage.py migrate

El default="" és necessari: la base de dades ja conté llibres i Django necessita saber què posar a la columna nova de les files existents (si no l'hi dones, makemigrations t'ho preguntarà interactivament).

Exercici 2:

# cataleg/views.py
def socis(request):
    return HttpResponse(
        "<h1>Socis de Papyrus</h1>"
        "<ul><li>Lluis — LLUIS-001</li>"
        "<li>Marta — MARTA-002</li>"
        "<li>Pau — PAU-003</li></ul>"
    )
# papyrus_web/urls.py
urlpatterns = [
    path("admin/", admin.site.urls),
    path("", views.inici),
    path("socis/", views.socis),
]

HTML en cadenes una altra vegada — igual d'incòmode que a Flask. Les plantilles Django que ho arreglen arriben a 10-05.

Exercici 3:

>>> from cataleg.models import Llibre
>>> for llibre in Llibre.objects.all():
...     print(f"{llibre.titol}: {llibre.preu_final(soci=True)}")
L'Odissea: 12.35
Hamlet: 9.83
El Quixot: 15.71
Faust: 20.75

Els quatre números canònics, ara sortint d'una base de dades en lloc d'un JSON. Llibre.objects.all() és el teu primer QuerySet: la sintaxi completa de consultes t'espera a la lliçó següent.

Conclusió

Django ha ensenyat les seves cartes: una estructura imposada però clara (settings.py configura, urls.py encamina, views.py respon, models.py defineix les dades), el patró MTV separant dades, lògica i presentació, i sobretot les bateries — el model Llibre que heretant de models.Model guanya la persistència que a Flask vas programar a mà sobre JSON, les migracions que versionen la forma de les teves dades, i aquell admin que va convertir la Marta en gestora de l'inventari amb tres línies de codi. La dataclass de M5 i el model Django són el mateix llibre amb coberta diferent: mateixes dades, mateixos preu_final() i hi_ha_estoc(), mateixos 12.35, 9.83, 15.71 i 20.75. Però de moment els visitants només veuen un HttpResponse amb HTML incrustat: res de catàleg navegable, ni cerques, ni altes públiques. A la propera lliçó completarem l'app: consultes amb QuerySets, plantilles amb herència (et sonaran moltíssim), formularis amb ModelForm que validen sols, i la web de Papyrus acabada per segona vegada — mateixa funcionalitat, dos camins, i tu sabent recórrer tots dos.

Curs de Programació en Python

Mòdul 1: Introducció a Python

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats