Vues génériques et compléments sur les migrations avec Django¶
Nous allons à présent explorer les vues génériques de Django qui sont aussi appelées Vues fondées sur des classes. Vous trouverez leur documentation sur le site de django. L’usage des vues génériques permet d’utiliser des vues associées à des classes en lieu et place des fonctions. Cela permet aussi de mieux organiser le code et de davantage le réutiliser en faisant parfois usage de mixins (héritage multiple) Nous approfondirons au passage notre compréhension des migrations Django.
Initialisation du projet¶
Vous allez créer un petit projet de gestion de musiques pour illuster tout ça !
django-admin startproject GestionMusiques
cd GestionMusiques
./manage.py startapp musiques
N’oubliez pas d’ajouter l’application musiques nouvellement créée dans la liste des apps installées, ainsi que de modifier le fichier urls.py :
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('musiques/', include('musiques.urls'))
]
Création d’un modèle simple¶
On veut pouvoir stocker des entités modélisant des morceaux de musique.
On va donc définir notre modèle dans le fichier models.py de l’app
musiques. Le modèle Morceau
ne requiert que des champs de type
texte, rendez vous sur la documentation officielle
pour de plus amples détails sur ce qu’il est possible de faire.
class Morceau(models.Model):
titre = models.CharField(max_lenght=64)
artiste = models.CharField(max_length=64)
def __str__(self):
return '{self.titre} ({self.artiste})'.format(self=self)
Mettez à jour le schéma de votre base de donnée avec le couple makemigrations/migrate. On reviendra sur ces deux commandes un peu plus tard.
./manage.py makemigrations
./manage.py migrate
Enfin, peuplez votre base de données avec quelques morceaux de bon goût
(à partir de la console Django lancée avec python manage.py shell
)
from musiques.models import Morceau
Morceau.objects.create(titre='Fruit de la passion', artiste='Francky Vincent')
Morceau.objects.create(titre='Space Oddity', artiste='David Bowie')
Afficher un morceau¶
Ajout d’un test¶
Ecrivons maintenant des tests afin de nous assurer que certaines routes existent dans notre app pour afficher les objets de la base de données. Aucune vue Django n’a encore été créée, il est donc évident que les tests vont échouer : c’est tout le principe du Test Driven Development (TDD). Les tests pour une fonctionnalité doivent être écrits avant l’implémentation de la fonctionnalité.
Plus pécisément, nous allons tester d’une part le fait qu’une url nommée existe pour afficher un objet, d’autre part que le traitement de la requête s’effectue sans erreur (code http 200).
from django.urls import reverse
from django.test import TestCase
from django.urls.exceptions import NoReverseMatch
from musiques.models import Morceau
class MorceauTestCase(TestCase):
def setUp(self):
Morceau.objects.create(titre='musique1', artiste='artise1')
Morceau.objects.create(titre='musique2', artiste='artise2')
Morceau.objects.create(titre='musique3', artiste='artise3')
def test_morceau_url_name(self):
try:
url = reverse('musiques:morceau-detail', args=[1])
except NoReverseMatch:
assert False
def test_morceau_url(self):
morceau = Morceau.objects.get(titre='musique1')
url = reverse('musiques:morceau-detail', args=[morceau.pk])
response = self.client.get(url)
assert response.status_code == 200
La commande ./manage.py test
devrait vous indiquer à ce stade que vos deux
tests ne passent pas.
Création d’une url¶
La première étape consiste à définir une nouvelle url dans notre app :
Direction le fichier musiques/urls.py :
from django.urls import path
from .views import morceau_detail
app_name = 'musiques' # Encapsule les urls de ce module dans le namespace musique
urlpatterns = [
path('<int:pk>', morceau_detail, name='morceau-detail')
]
La vue morceau_detail
n’existe pas, vous allez donc en créer une qui ne fait rien !
def morceau_detail(request, pk):
pass
La commande ./manage.py test
devrait à présent vous informer qu’un test ne passe pas (au lieu des 2).
Et vous pouvez compléter :
from django.shortcuts import render
from django.http import HttpResponse
def morceau_detail(request,pk):
return HttpResponse('OK')
… pour passer enfin les 2 tests !!
Création de la vue¶
Vous allez implémenter la vue qui permettra d’afficher une instance du
modèle Morceau
. Pour cela, vous allez utiliser une vue générique basée sur une classe.
Django fournit différentes classes génériques pour représenter des vues, comme DetailView
ou ListView
.
C’est la classe DetailView
qui va vous intéresser ici puisqu’elle
permet d’associer une vue à un model. D’autre classes existent pour
répondre à d’autres problématiques, vous en trouverez la liste sur la
documentation officielle.
from django.views.generic import DetailView
from .models import Morceau
class MorceauDetailView(DetailView):
model = Morceau
La vue Django est créée, à présent il faut déterminer comment l’afficher.
C’est le template qui va s’en charger. Par défaut, DetailView
va chercher
un template nommé selon le nom du model renseigné.
Ici ce template s’appelle morceau_detail.html et doit être
situé dans le dossier templates/musiques de l’app musiques.
Créez à présent ce fichier et remplissez le.
L’objet attendu s’appelle object ou morceau
La dernière étape consiste à modifier le fichier musiques/urls.py. La
méthode morceau_detail
n’existe plus, il faut alors indiquer que
c’est la classe MorceauDetailView
qui va se charger de la requête.
from .views import MorceauDetailView
app_name = 'musiques'
urlpatterns = [
path('<int:pk>', MorceauDetailView.as_view(), name='morceau-detail')
]
Notez la différence, la fonction path
attend une fonction en deuxième paramètre, MorceauDetailView
étant une classe, on doit
utiliser la méthode de classe as_view
pour que path
fonctionne correctement.
A présent, la commande ./manage.py test
devrait indiquer que tous les tests passent.
Complétons le template morceau_detail.html :
<ul>
<li>{{object.titre}}</li>
<li>{{object.artiste}}</li>
</ul>
<a href="{% url 'musiques:morceau_list' %}">
Revenir a la liste
</a>
Notez l’usage de {% url 'musiques:morceau_list' %}
pour faire référence à la route « morceau_list » que nous allons ajouter à présent à notre app musiques :
ListView¶
Ajoutez à vos views :
from django.views.generic import ListView
class MorceauList(ListView):
model = Morceau
Puis à urls.py :
from django.urls import path
from .views import MorceauDetailView, MorceauList
app_name = 'musiques'
urlpatterns = [
path('<int:pk>', MorceauDetailView.as_view(), name='morceau_detail'),
path('', MorceauList.as_view(), name='morceau_list'),
]
Avec le petit template correspondant morceau_list.html à placer dans musiques/templates/musiques :
<h2>Morceaux</h2>
<ul>
{% for morceau in morceau_list %}
<li>
<h3>Morceau n°{{morceau.pk}}</h3>
{{ morceau.titre }} par {{ morceau.artiste }}
</li>
{% endfor %}
</ul>
Testez sur http://localhost:8000/musiques !
Complexification du modèle¶
Le modèle Morceau
est très pauvre, beaucoup d’informations manquent.
Par exemple, on ne connait pas la date de sortie d’un morceau donné.
Heureusement, grâce au système de migrations, le schéma de notre base
de données n’est pas figé, on va pouvoir le modifier.
Ajout d’un champ nullable¶
On va dans un premier temps compléter notre modèle en lui ajoutant un champ date_sortie. Modifiez votre fichier musiques/models.py en ajoutant ce champ date :
date_sortie = models.DateField(null=True)
Il est nécessaire de spécifier null=True
car lorsque le moteur de
migration ajoutera le champ dans la table, il ne saura pas quelle valeur
attribuer à la colonne nouvellement créée. Il mettra donc une valeur
nulle par défaut. Il est également possible de spécifier blank=True
.
Qu’est-ce que cela signifie ? Recherchez dans la doc de Django.
Ensuite, la commande ./manage.py makemigrations musiques
va scanner
le fichier musiques/models.py pour détecter les changements entre la
version courante du modèle et la nouvelle (celle avec le champ date).
A la suite de cette commande un fichier est créé dans le répertoire
musiques/migrations.
Celui-ci est un fichier Python indiquant la marche à suivre pour passer de la version 1 du schéma de base de données
à la version 2.
Pour appliquer ces changements, on utilise la commande ./manage.py migrate
qui va lire ce fichier et appliquer les
modifications nécessaires.
Utilisez ./manage.py sqlmigrate musiques 0002
pour avoir un aperçu du code SQL appliquant la modification.
Déplacement d’un champ¶
Vous avez certainement déjà entendu parler du problème de la redondance des données dans une base de données relationnelle. Ce problème apparait lorsqu’une même donnée figure plusieurs fois dans la base de donnée sous des formes différentes. Si on enregistre deux morceaux de musique avec le même artiste, l’information contenue dans la colonne artiste devient redondante : on aimerait pouvoir lier un morceau avec un artiste, qui ne serait créé qu’une seule fois.
On va donc créer une nouvelle classe représentant un artiste dans le fichier musiques/models.py
class Artiste(models.Model):
nom = models.CharField(max_length=64)
On va ensuite générer un fichier de migration avec la commande makemigrations puis appliquer cette migration avec migrate :
./manage.py makemigrations
./manage.py migrate
Cela aura pour conséquence de générer une table Artiste vide.
Vous allez à présent peupler cette nouvelle table à l’aide d’une migration de données, c’est à dire une migration n’impactant pas le schéma de la base, mais uniquement ses données.
Créez une migration vide avec
./manage.py makemigrations --empty musiques
, ce qui aura pour effet
de créer le fichier:
musiques/migrations/0004_auto_date_num.py
Ouvrez ce fichier et constatez qu’une liste vide est affectée à la
variable operations
: cela signifie que cette migration, en l’état,
ne fait rien. Vous allez indiquez à Django comment il doit migrer les
données de votre base, grâce à la méthode migrations.RunPython
.
Cette méthode doit recevoir une fonction définissant le comportement de
la migration.
Il est souvent sécurisant de spécifier le paramètre reverse_code
qui permettra à Django d’annuler la migration en cas de problème et de
revenir au schéma antérieur.
from django.db import migrations
def migrer_artiste(apps, schema):
# On récupère les modèles
Morceau = apps.get_model('musiques', 'Morceau')
Artiste = apps.get_model('musiques', 'Artiste')
# On récupère les artistes déjà enregsitrés dans la table Morceau
# Voir la documentation :
# - https://docs.djangoproject.com/fr/3.1/ref/models/querysets/#all
# - https://docs.djangoproject.com/fr/3.1/ref/models/querysets/#values
# - https://docs.djangoproject.com/fr/3.1/ref/models/querysets/#distinct
artistes_connus = [fields['artiste']
for fields in Morceau.objects.all().values('artiste').distinct()]
# On peuple la table Artiste
for artiste in artistes_connus:
Artiste.objects.create(nom=artiste)
def annuler_migrer_artiste(apps, schema):
Artiste = apps.get_model('musiques', 'Artiste')
Artiste.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('musiques', '0003_artiste'),
]
operations = [
migrations.RunPython(migrer_artiste,
reverse_code=annuler_migrer_artiste)
]
Prenez l’habitude de définir la fonction de retour en arrière
(reverse_code
) quand c’est possible, ça ne coûte rien et si jamais
une erreur a été faite lors de la migration, elle pourra être annulée
par un retour en arrière.
A présent, appliquez la migration et vérifiez que la table Artiste n’est
pas vide. Retournez à un état antérieur avec la commande
./manage.py migrate musiques 0003
et observez, d’une part que ça
marche car on a spécifié l’opération de retour en arrière et d’autre
part que la table Artiste est vide. Réappliquez les migrations avec
./manage.py migrate musiques
.
Export/Import de données¶
Notre base doit contenir quelques données. Pour les exporter simplement au format json et disposer rapidement d’un petit jeu de données :
./manage.py dumpdata --format=json musiques > initial_musiques_data.json
On pourra ainsi en disposer pour recréer les données avec :
./manage.py loaddata initial_musiques_data.json
Gitpod¶
Le code correspondant se trouve sur Gitlab
Le projet est disponible, prêt à être utilisé et complété sur gitpod :

Travail supplémentaire¶
Il reste encore plusieurs migrations à faire :
Ajouter un champ nullable de type
ForeignKey
dans la table MorceauEffectuer une migration de données pour lier la clef étrangère à la bonne entrée de la table Artiste
Supprimer le champ artiste de la table Morceau et rendre la nouvelle clef étrangère non nullable
A vous d’écrire ces trois migrations !
Complétez aussi votre application en proposant un CRUD des Morceaux en utilisant des Vues basées sur des classes (comme des CreateView ou UpdateView) et avec un beau CSS. Ajoutez les tests correspondants (fonctionnels ou autres).
Code complet¶
Le code complet se trouve sur Github Il inclut des templates Bootswatch et des icones fournies par fontawesome.
Le projet est disponible, prêt à être utilisé et complété sur gitpod :
