Cours Complet CRUD Django
Cours complet CRUD Django,
Etapes :
- Rappels & vocabulaire CRUD
- Préparer le projet Django
- Créer le modèle (Model)
- Migrer la base de données
- Afficher la liste (Read – List)
- Créer un objet (Create)
- Modifier un objet (Update)
- Supprimer un objet (Delete)
- Améliorations pro : messages, validation, pagination, recherche
- Sécurité & permissions
- Structure propre du code
- Tester ton CRUD (tests unitaires)
- Rappels & vocabulaire CRUD
CRUD = Create, Read, Update, Delete :
- Create : créer un nouvel enregistrement en base
- Read : lire/lister/voir les détails
- Update : modifier un enregistrement
- Delete : supprimer un enregistrement
En Django, un CRUD pro implique de bien maîtriser :
- Models (fichier
models.py) - URLs (fichier
urls.py) - Views (fichier
views.py, FBV ou CBV) - Templates (fichiers HTML, souvent dans
templates/) - Forms / ModelForms
- Messages, validations, pagination, permissions, etc.
2. Préparer le projet Django
⚙️ Préparer l’environnement virtuel avant de créer le projet Django
Avant de créer ton projet Django, il est presque obligatoire de travailler dans un environnement virtuel pour isoler tes dépendances.
. Créer l’environnement virtuel
python3 -m venv env
Activer l’environnement virtuel
- Sur Windows :
env\Scripts\activate
- Sur macOS / Linux :
source env/bin/activate
Tu verras maintenant (env) au début de ta ligne de commande — c’est bon signe.
. Installer Django dans cet environnement
pip install django
. Vérifier l’installation
python -m django --version
À partir de là, tu peux créer ton projet Django proprement :
django-admin startproject myproject
a) Créer un projet
django-admin startproject myproject
cd myproject
python manage.py runserver
Tu testes que ça marche : http://127.0.0.1:8000/
b) Créer une app
On va faire un CRUD sur une ressource simple, par exemple Livre.
python manage.py startapp books
Dans myproject/settings.py, ajoute l’app :
INSTALLED_APPS = [
# ...
'books',
]
3. Créer le modèle (Model)
Dans books/models.py :
from django.db import models
class Book(models.Model):
titre = models.CharField(max_length=200)
auteur = models.CharField(max_length=200)
date_publication = models.DateField(null=True, blank=True)
prix = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
description = models.TextField(blank=True)
# timestamps pro
created_at = models.DateTimeField(auto_now_add=True) # à la création
updated_at = models.DateTimeField(auto_now=True) # à chaque modification
def __str__(self):
return f"{self.titre} - {self.auteur}"
Points importants (pro) :
null=True= champ peut être NULL en baseblank=True= champ peut être vide dans les formulairesauto_now_addetauto_nowutiles pour tracking
4. Migrer la base de données
Créer les migrations :
python manage.py makemigrations
python manage.py migrate
5. Afficher la liste des livres (Read – List)
a) Configurer les URLs
Dans myproject/urls.py :
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('books/', include('books.urls')), # on délègue à l’app
]
Créer books/urls.py :
from django.urls import path
from . import views
app_name = 'books'
urlpatterns = [
path('', views.book_list, name='book_list'),
]
b) View pour la liste (FBV – Function-Based View)
Dans books/views.py :
from django.shortcuts import render
from .models import Book
def book_list(request):
books = Book.objects.all().order_by('-created_at') # les plus récents d’abord
return render(request, 'books/book_list.html', {'books': books})
c) Template HTML
Créer le dossier books/templates/books/book_list.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Liste des livres</title>
</head>
<body>
<h1>Liste des livres</h1>
<a href="{% url 'books:book_create' %}">+ Ajouter un livre</a>
<table border="1">
<tr>
<th>Titre</th>
<th>Auteur</th>
<th>Date</th>
<th>Actions</th>
</tr>
{% for book in books %}
<tr>
<td>{{ book.titre }}</td>
<td>{{ book.auteur }}</td>
<td>{{ book.date_publication|default:"-" }}</td>
<td>
<a href="{% url 'books:book_detail' book.id %}">Voir</a> |
<a href="{% url 'books:book_update' book.id %}">Modifier</a> |
<a href="{% url 'books:book_delete' book.id %}">Supprimer</a>
</td>
</tr>
{% empty %}
<tr><td colspan="4">Aucun livre pour le moment.</td></tr>
{% endfor %}
</table>
</body>
</html>
Notions clés :
{% url 'books:book_detail' book.id %}= usage deapp_name+name|default:"-"= filtre template
6. Créer un livre (Create)
a) Formulaire avec ModelForm
Dans books/forms.py :
from django import forms
from .models import Book
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['titre', 'auteur', 'date_publication', 'prix', 'description']
widgets = {
'date_publication': forms.DateInput(attrs={'type': 'date'}),
'description': forms.Textarea(attrs={'rows': 4}),
}
labels = {
'titre': 'Titre du livre',
'auteur': 'Auteur',
'date_publication': 'Date de publication',
'prix': 'Prix (en €)',
'description': 'Description',
}
help_texts = {
'description': 'Quelques lignes sur le contenu du livre.',
}
Points pro :
widgetspour contrôler le HTMLlabelsethelp_textspour un UI propre
b) URL pour create
Dans books/urls.py :
urlpatterns = [
path('', views.book_list, name='book_list'),
path('create/', views.book_create, name='book_create'),
# on ajoutera detail/update/delete après
]
c) View create
Dans books/views.py :
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import BookForm
def book_create(request):
if request.method == 'POST':
form = BookForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, "Livre créé avec succès.")
return redirect('books:book_list')
else:
form = BookForm()
return render(request, 'books/book_form.html', {'form': form, 'title': 'Ajouter un livre'})
Notions pro :
request.method == 'POST'form.is_valid()→ validation côté serveurmessages.successpour feedback utilisateur
d) Template book_form.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Enregistrer</button>
</form>
<a href="{% url 'books:book_list' %}">← Retour à la liste</a>
</body>
</html>
Notion clé :
{% csrf_token %}= OBLIGATOIRE pour sécuriser les formulaires POST
7. Détail & Modification (Read – Detail + Update)
a) Détail d’un livre (Read – Detail)
URL :
urlpatterns = [
path('', views.book_list, name='book_list'),
path('create/', views.book_create, name='book_create'),
path('<int:pk>/', views.book_detail, name='book_detail'),
path('<int:pk>/update/', views.book_update, name='book_update'),
]
View :
from django.shortcuts import get_object_or_404
def book_detail(request, pk):
book = get_object_or_404(Book, pk=pk)
return render(request, 'books/book_detail.html', {'book': book})
Template book_detail.html :
<h1>{{ book.titre }}</h1>
<p>Auteur : {{ book.auteur }}</p>
<p>Date de publication : {{ book.date_publication|default:"-" }}</p>
<p>Prix : {{ book.prix|default:"-" }}</p>
<p>Description : {{ book.description|default:"Aucune description." }}</p>
<a href="{% url 'books:book_update' book.id %}">Modifier</a> |
<a href="{% url 'books:book_delete' book.id %}">Supprimer</a> |
<a href="{% url 'books:book_list' %}">Retour à la liste</a>
b) Modifier un livre (Update)
View :
def book_update(request, pk):
book = get_object_or_404(Book, pk=pk)
if request.method == 'POST':
form = BookForm(request.POST, instance=book)
if form.is_valid():
form.save()
messages.success(request, "Livre modifié avec succès.")
return redirect('books:book_detail', pk=book.pk)
else:
form = BookForm(instance=book)
return render(request, 'books/book_form.html', {'form': form, 'title': 'Modifier le livre'})
Points pro :
instance=bookpour lier le formulaire à l’objet existant- On réutilise le même template
book_form.htmlpour create & update
8. Supprimer un livre (Delete)
URL :
urlpatterns = [
path('', views.book_list, name='book_list'),
path('create/', views.book_create, name='book_create'),
path('<int:pk>/', views.book_detail, name='book_detail'),
path('<int:pk>/update/', views.book_update, name='book_update'),
path('<int:pk>/delete/', views.book_delete, name='book_delete'),
]
View :
from django.views.decorators.http import require_POST
def book_delete(request, pk):
book = get_object_or_404(Book, pk=pk)
if request.method == 'POST':
book.delete()
messages.success(request, "Livre supprimé avec succès.")
return redirect('books:book_list')
# on affiche une page de confirmation
return render(request, 'books/book_confirm_delete.html', {'book': book})
Template book_confirm_delete.html :
<h1>Confirmer la suppression</h1>
<p>Es-tu sûr de vouloir supprimer : <strong>{{ book.titre }}</strong> ?</p>
<form method="post">
{% csrf_token %}
<button type="submit">Oui, supprimer</button>
<a href="{% url 'books:book_detail' book.id %}">Annuler</a>
</form>
En prod, ne JAMAIS supprimer sans page de confirmation.
9. Améliorations pro : recherche, pagination, messages, validation
a) Pagination
Dans la view book_list :
from django.core.paginator import Paginator
def book_list(request):
qs = Book.objects.all().order_by('-created_at')
paginator = Paginator(qs, 10) # 10 livres par page
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'books/book_list.html', {'page_obj': page_obj})
Template book_list.html (adapter la boucle) :
{% for book in page_obj %}
<!-- même contenu -->
{% empty %}
<tr><td colspan="4">Aucun livre.</td></tr>
{% endfor %}
<div>
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Précédent</a>
{% endif %}
Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Suivant</a>
{% endif %}
</div>
b) Recherche et filtrage
Dans book_list :
def book_list(request):
qs = Book.objects.all().order_by('-created_at')
query = request.GET.get('q')
if query:
qs = qs.filter(titre__icontains=query)
paginator = Paginator(qs, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'query': query,
}
return render(request, 'books/book_list.html', context)
Dans le template, ajouter un formulaire de recherche :
<form method="get">
<input type="text" name="q" placeholder="Rechercher un livre..." value="{{ query|default:'' }}">
<button type="submit">Rechercher</button>
</form>
c) Validation custom dans le ModelForm
Dans forms.py :
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['titre', 'auteur', 'date_publication', 'prix', 'description']
def clean_prix(self):
prix = self.cleaned_data.get('prix')
if prix is not None and prix < 0:
raise forms.ValidationError("Le prix ne peut pas être négatif.")
return prix
Validation pro = ne jamais faire confiance aux données venant du client.
10. Sécurité & permissions
Pour un CRUD pro, en général :
- Seules les personnes authentifiées peuvent créer / modifier / supprimer.
- Parfois, seulement le propriétaire de l’objet.
a) Protéger avec login_required
Dans views.py :
from django.contrib.auth.decorators import login_required
@login_required
def book_create(request):
# ...
Pareil pour book_update et book_delete.
Dans settings.py :
LOGIN_URL = 'login' # route de login
b) Vérifier que l’utilisateur est propriétaire (si tu ajoutes un champ owner)
Dans le modèle :
from django.contrib.auth.models import User
class Book(models.Model):
# ...
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books')
Dans book_create :
if form.is_valid():
book = form.save(commit=False)
book.owner = request.user
book.save()
Dans book_update / book_delete :
from django.http import HttpResponseForbidden
def book_update(request, pk):
book = get_object_or_404(Book, pk=pk)
if book.owner != request.user:
return HttpResponseForbidden("Tu n’as pas le droit de modifier ce livre.")
# suite...
11. Version pro avec Class-Based Views (CBV + Generic Views)
En pro, on utilise beaucoup les generic class-based views de Django :
ListView→ listeDetailView→ détailCreateView→ créationUpdateView→ mise à jourDeleteView→ suppression
Exemple pour Book :
Dans views.py :
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Book
from .forms import BookForm
class BookListView(ListView):
model = Book
template_name = 'books/book_list.html'
context_object_name = 'books'
paginate_by = 10
ordering = ['-created_at']
class BookDetailView(DetailView):
model = Book
template_name = 'books/book_detail.html'
context_object_name = 'book'
class BookCreateView(LoginRequiredMixin, CreateView):
model = Book
form_class = BookForm
template_name = 'books/book_form.html'
success_url = reverse_lazy('books:book_list')
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)
class BookUpdateView(LoginRequiredMixin, UpdateView):
model = Book
form_class = BookForm
template_name = 'books/book_form.html'
def get_success_url(self):
return reverse_lazy('books:book_detail', kwargs={'pk': self.object.pk})
class BookDeleteView(LoginRequiredMixin, DeleteView):
model = Book
template_name = 'books/book_confirm_delete.html'
success_url = reverse_lazy('books:book_list')
Les URLs :
urlpatterns = [
path('', BookListView.as_view(), name='book_list'),
path('create/', BookCreateView.as_view(), name='book_create'),
path('<int:pk>/', BookDetailView.as_view(), name='book_detail'),
path('<int:pk>/update/', BookUpdateView.as_view(), name='book_update'),
path('<int:pk>/delete/', BookDeleteView.as_view(), name='book_delete'),
]
12. Tester ton CRUD (test unitaire basique)
Dans books/tests.py :
from django.test import TestCase
from django.urls import reverse
from .models import Book
class BookCRUDTest(TestCase):
def test_create_book(self):
response = self.client.post(reverse('books:book_create'), {
'titre': 'Mon livre',
'auteur': 'Auteur X',
'prix': '10.00'
})
# redirection après création
self.assertEqual(response.status_code, 302)
self.assertEqual(Book.objects.count(), 1)
def test_list_books(self):
Book.objects.create(titre='L1', auteur='A1')
response = self.client.get(reverse('books:book_list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'L1')
Lancer les tests :
python manage.py test