Projet IMDB - Application Flask MVC
L'application Flask qui vous a été transmise permet de consulter une base de données IMDB (films et acteurs) stockée dans PostgreSQL.
C'est dur au debut
Il est normal de trouver cette activite dure au debut, car il faut penser a plein de choses en meme temps. Moyennant une implication constante, vous passerez un cap, non seulement dans la comprehension des concepts, mais encore mieux, dans leur assimilation perenne.
Les explications techniques sont volontairement legeres, afin de ne pas alourdir l'acces a la comprehension globale (le point de cette activite).
Je ne veux pas voir de cosmetique
L'application utilise deja Bulma CSS pour l'apparence. Ne perdez pas de temps a modifier le style, concentrez-vous sur la logique metier.
Architecture MVC
L'application suit une architecture Modele-Vue-Controleur :
imdb/
├── app.py # Point d'entree Flask
├── database.py # Connexion PostgreSQL (pool)
├── modele.py # Modele : dataclasses (Acteur, Film...)
├── controlers/
│ └── acteurs_controler.py # Controleur : gere les routes
├── services/
│ └── acteurs_db.py # Service : requetes SQL
└── templates/
├── squelette.html.j2 # Vue : template de base
├── liste_acteurs.html.j2 # Vue : liste des acteurs
└── detail_acteur.html.j2 # Vue : detail d'un acteur
| Couche | Fichier(s) | Role |
|---|---|---|
| Modele | modele.py |
Definit les structures de donnees (dataclasses) |
| Vue | templates/*.j2 |
Genere le HTML a partir des donnees |
| Controleur | controlers/*.py |
Recoit les requetes, orchestre modele et vue |
| Service | services/*.py |
Accede a la base de donnees |
Client vs Serveur : Qui fait quoi ?
C'est fondamental de comprendre ce qui s'execute ou.
Ce qui s'execute sur le SERVEUR (Python/Flask)
- Le code Python (
app.py,controlers/,services/) - Les requetes SQL vers PostgreSQL
- Le rendu des templates Jinja (generation du HTML)
- La logique metier (calculs, validations...)
Ce qui s'execute dans le NAVIGATEUR (client)
- L'affichage du HTML recu
- L'execution du JavaScript (menu burger)
- Les styles CSS (Bulma)
- Les interactions utilisateur (clics, formulaires)
Piege classique
Jinja ({{ variable }}, {% for %}) s'execute sur le serveur, pas dans le navigateur !
Quand le navigateur recoit la page, tout le code Jinja a deja ete remplace par du HTML pur.
Diagramme de sequence
Voici ce qu'il se passe quand un utilisateur clique sur un lien :
sequenceDiagram
box Aqua Ce qu'il se passe sur le client
participant Utilisateur
participant Navigateur
end
box Aqua Ce qu'il se passe sur le serveur
participant Flask
participant Donnees
participant Jinja
end
Utilisateur->>+Navigateur: Clique
Navigateur->>Navigateur: genereRequete()
Navigateur->>+Flask: envoi(Requete HTTP)
Flask->>Flask: Cherche la route a executer
Flask->>+Donnees: demandeData(parametres)
Donnees-->>-Flask: data
Flask->>+Jinja: demandeHTML(data)
Jinja->>Jinja: traiteTemplate()
Jinja-->>-Flask: renvoieHTML
Flask-->>-Navigateur: reponse HTTP contenant le HTML
Navigateur->>Navigateur: Generation de l'affichage ecran
Navigateur->>-Utilisateur: affichage
Les fichiers cles
1. app.py - Point d'entree
from flask import Flask
from controlers.acteurs_controler import acteurs_bp
app = Flask(__name__)
app.register_blueprint(acteurs_bp, url_prefix='/acteurs')
@app.route('/')
def index():
return flask.redirect(flask.url_for('acteurs_bp.liste_acteurs'))
Le register_blueprint connecte le controleur des acteurs avec le prefixe /acteurs.
Ainsi, la route / du blueprint devient /acteurs/.
2. controlers/acteurs_controler.py - Le controleur
@acteurs_bp.route('/<string:nconst>', methods=['GET'])
def detail_acteur(nconst: str):
# 1. Demander les donnees au service
acteur = svc.get_acteur(nconst)
films = svc.get_films_acteur(nconst)
# 2. Generer le HTML avec le template
return render_template('detail_acteur.html.j2',
acteur=acteur,
films=films)
Le controleur :
- Recoit la requete HTTP
- Extrait les parametres (ici
nconstdepuis l'URL) - Appelle le service pour obtenir les donnees
- Passe les donnees au template pour generer le HTML
3. services/acteurs_db.py - Le service
def get_acteur(nconst: str) -> Acteur | None:
with database.get_connection() as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT nconst, primary_name, birth_year, death_year
FROM name
WHERE nconst = %s
""", (nconst,))
row = cur.fetchone()
if row is None:
return None
return Acteur(row[0], row[1], row[2], row[3])
Le service :
- Ouvre une connexion a la base de donnees
- Execute une requete SQL parametree (securisee contre l'injection SQL)
- Transforme le resultat en objet Python (dataclass)
4. templates/detail_acteur.html.j2 - La vue
{% extends "squelette.html.j2" %}
{% block title %}{{ acteur.primary_name }}{% endblock %}
{% block main %}
<h1>{{ acteur.primary_name }}</h1>
{% for film, principal in films %}
<p>{{ film.primary_title }} ({{ film.start_year }})</p>
{% endfor %}
{% endblock %}
Le template Jinja :
{% extends %}: herite d'un template parent{{ variable }}: affiche une valeur{% for %}: boucle sur un itérable{% if %}: condition
Demarrer l'application
- Configurer le fichier
.envavec vos identifiants PostgreSQL - Installer les dependances :
uv add flask psycopg[binary] psycopg-pool python-dotenv - Lancer :
flask run --debug - Ouvrir http://127.0.0.1:5000
Exercices de prise en main
Methode
Pour chaque exercice :
- Identifiez quel(s) fichier(s) modifier
- Faites la modification minimale
- Testez immediatement dans le navigateur
- Commitez si ca fonctionne
Exercice 1 : Modifier un texte
Changer le titre de la page d'accueil
Dans la liste des acteurs, le titre affiche "Liste des acteurs".
Mission : Changez ce titre en "Acteurs et actrices IMDB"
Indice : Regardez dans templates/liste_acteurs.html.j2
Solution
Dans templates/liste_acteurs.html.j2, ligne 8 :
Exercice 2 : Ajouter une colonne
Afficher la duree de vie
Dans le tableau de la liste des acteurs, on voit l'annee de naissance et l'annee de deces.
Mission : Ajoutez une colonne "Age" qui affiche :
- L'age au deces si l'acteur est decede
- Rien sinon
Indice : C'est un calcul Jinja {{ acteur.death_year - acteur.birth_year }}
Solution
Dans templates/liste_acteurs.html.j2 :
-
Ajoutez l'en-tete de colonne :
-
Ajoutez la cellule dans la boucle :
Exercice 3 : Comprendre le chemin des donnees
Tracez le parcours d'une requete
Quand vous accedez a /acteurs/nm0000001 :
- Quel fichier recoit la requete en premier ?
- Quelle fonction du controleur est appelee ?
- Quelle(s) fonction(s) du service sont appelees ?
- Quel template genere le HTML ?
Solution
app.pyrecoit la requete, reconnait le prefixe/acteurset la transmet au blueprintdetail_acteur(nconst)dansacteurs_controler.pyget_acteur(),get_films_acteur(),get_professions_acteur()dansacteurs_db.pydetail_acteur.html.j2
Exercice 4 : Modifier le nombre de resultats par page
Changer la pagination
Par defaut, la liste affiche 50 acteurs par page.
Mission : Passez a 20 acteurs par page.
Indice : Cherchez la variable limit dans le controleur.
Exercice 5 : Ajouter une information
Afficher le nombre de films dans la liste
Dans la liste des acteurs, on ne voit que le nom et les dates.
Mission : Ajoutez une colonne "Nb films" qui affiche le nombre de films de chaque acteur.
Attention : Cet exercice necessite de modifier plusieurs fichiers !
Solution
-
Modifier le service (
services/acteurs_db.py) - ajouter une fonction : -
Modifier le controleur pour enrichir les donnees... Ou mieux : modifier la requete SQL pour inclure le count directement !
C'est plus complexe. La vraie solution serait de modifier get_all_acteurs()
pour faire une jointure avec COUNT.
Exercice 6 : Classement des films par score (moyenne bayesienne)
Pourquoi la moyenne simple ne suffit pas ?
Imaginez deux films :
- Film A : note moyenne de 9.5/10 avec 3 votes
- Film B : note moyenne de 8.2/10 avec 50 000 votes
Lequel est vraiment le meilleur ? Le film A a une meilleure moyenne, mais avec seulement 3 votes, cette note n'est pas fiable. Le film B a une note legerement inferieure, mais basee sur beaucoup plus d'avis.
C'est le probleme que resout la moyenne bayesienne.
Le principe de la moyenne bayesienne
IMDB utilise cette formule pour calculer un score pondere :
Ou :
| Variable | Signification |
|---|---|
| R | Note moyenne du film (average_rating) |
| v | Nombre de votes du film (num_votes) |
| m | Seuil minimum de votes (ex: 1000) |
| C | Moyenne globale de tous les films |
Interpretation intuitive :
- Si un film a peu de votes (v petit), son score est "tire" vers la moyenne globale C
- Si un film a beaucoup de votes (v grand), son score se rapproche de sa vraie moyenne R
- Le parametre m controle a partir de combien de votes on fait confiance a la note
Exemple concret :
Supposons C = 7.0 (moyenne globale) et m = 1000 (seuil).
| Film | R (moyenne) | v (votes) | Calcul | Score |
|---|---|---|---|---|
| Film A | 9.5 | 3 | (3/1003)9.5 + (1000/1003)7.0 | 7.01 |
| Film B | 8.2 | 50000 | (50000/51000)8.2 + (1000/51000)7.0 | 8.18 |
Le film B obtient un meilleur score car sa note est basee sur beaucoup plus de votes !
Mission
Etape 1 : Calculer la moyenne globale
Notez cette valeur (environ 6.9 pour IMDB).
Etape 2 : Ajouter une colonne score a la table title
Etape 3 : Mettre a jour les scores avec la formule bayesienne Cet update s'appelle un subcorrelated update. c'est completement hors programme. Ca permet de mettre à jour une colonne en fonction de données dans plusieurs tables en effectuant des jointures.
UPDATE title t
SET score = (
SELECT
(r.num_votes::float / (r.num_votes + 1000)) * r.average_rating
+ (1000.0 / (r.num_votes + 1000)) * 6.9
FROM rating r
WHERE r.tconst = t.tconst
)
WHERE EXISTS (SELECT 1 FROM rating r WHERE r.tconst = t.tconst);
Explication du SQL
::float: convertit en nombre decimal (PostgreSQL)1000: notre seuil m6.9: la moyenne globale C (a adapter selon votre base)WHERE EXISTS: ne met a jour que les films qui ont une note
Etape 4 : Verifier le resultat
SELECT primary_title, start_year, score
FROM title
WHERE score IS NOT NULL
ORDER BY score DESC
LIMIT 20;
Films attendus dans le top
Vous devriez voir des films celebres et bien notes avec beaucoup de votes : The Shawshank Redemption, The Godfather, The Dark Knight, etc.
Etape 5 : Integrer dans l'application
- Modifier la dataclass
Filmdansmodele.pypour ajouter l'attributscore - Modifier les requetes SQL dans
films_db.pypour inclure le score - Ajouter une option de tri par score dans la liste des films
Pour aller plus loin
- Experimentez avec differentes valeurs de m (500, 2000, 5000...)
- Observez comment cela change le classement
- Reflechissez : quel m serait adapte pour un site avec peu d'utilisateurs ?
Mission finale : Creer une page de recherche de films
Maintenant que vous avez compris l'architecture, votre mission est de creer une fonctionnalite complete : la recherche et l'affichage des films.
Objectif
Creer deux pages :
- Liste/Recherche de films (
/films/) : affiche les films avec un champ de recherche - Detail d'un film (
/films/<tconst>) : affiche les informations d'un film et son casting
Etapes guidees
Etape 1 : Creer le service services/films_db.py
Creez un nouveau fichier avec les fonctions :
import database
from modele import Film, Principal, Acteur
def get_all_films(page: int = 1, limit: int = 50) -> list[Film]:
"""Retourne une liste paginee de films"""
# A completer : similaire a get_all_acteurs
def count_films() -> int:
"""Retourne le nombre total de films"""
# A completer
def search_films(search: str, page: int = 1, limit: int = 50) -> list[Film]:
"""Recherche des films par titre"""
# A completer
def count_films_search(search: str) -> int:
"""Compte les films correspondant a la recherche"""
# A completer
def get_film(tconst: str) -> Film | None:
"""Retourne un film par son ID"""
# A completer
def get_acteurs_film(tconst: str) -> list[tuple[Acteur, Principal]]:
"""Retourne les acteurs d'un film avec leur role"""
# A completer : similaire a get_films_acteur mais inverse
Tables utiles
title: contient les films (tconst, primary_title, start_year, ...)principal: table de liaison entre films et acteursname: contient les acteurs
Etape 2 : Creer le controleur controlers/films_controler.py
from flask import Blueprint, request, render_template, abort
import services.films_db as svc
films_bp = Blueprint('films_bp', __name__)
@films_bp.route('/', methods=['GET'])
def liste_films():
"""Liste paginee des films avec recherche"""
# A completer : similaire a liste_acteurs
@films_bp.route('/<string:tconst>', methods=['GET'])
def detail_film(tconst: str):
"""Detail d'un film"""
# A completer : similaire a detail_acteur
Etape 3 : Enregistrer le blueprint dans app.py
from controlers.films_controler import films_bp
app.register_blueprint(films_bp, url_prefix='/films')
Etape 4 : Creer les templates
templates/liste_films.html.j2: inspirez-vous deliste_acteurs.html.j2templates/detail_film.html.j2: inspirez-vous dedetail_acteur.html.j2
Criteres de reussite
- [ ] La page
/films/affiche une liste de films paginee - [ ] La recherche par titre fonctionne
- [ ] Cliquer sur un film affiche sa page de detail
- [ ] La page de detail affiche : titre, annee, type, duree, et liste des acteurs
- [ ] Cliquer sur un acteur dans le detail d'un film mene a sa page de detail
Bonus
- [ ] Ajouter un lien vers la fiche IMDB officielle :
https://www.imdb.com/title/{tconst} - [ ] Afficher la note du film si disponible (table
rating) - [ ] Filtrer par type de film (movie, tvSeries, etc.)
- [ ] Dans le detail d'un acteur, rendre les titres de films cliquables vers
/films/<tconst>
Rappel : Structure de la base de donnees
| Table | Description | Colonnes principales |
|---|---|---|
name |
Acteurs/realisateurs | nconst, primary_name, birth_year, death_year |
title |
Films/series | tconst, primary_title, title_type, start_year, runtime_minutes |
principal |
Liaisons film-acteur | tconst, nconst, category, characters |
rating |
Notes des films | tconst, average_rating, num_votes |
genre |
Genres | genre_id, name |
title_genre |
Liaison film-genre | tconst, genre_id |