Cours complet CRUD Django,

Etapes :

  1. Rappels & vocabulaire CRUD
  2. Préparer le projet Django
  3. Créer le modèle (Model)
  4. Migrer la base de données
  5. Afficher la liste (Read – List)
  6. Créer un objet (Create)
  7. Modifier un objet (Update)
  8. Supprimer un objet (Delete)
  9. Améliorations pro : messages, validation, pagination, recherche
  10. Sécurité & permissions
  11. Structure propre du code
  12. Tester ton CRUD (tests unitaires)

  1. 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 base
  • blank=True = champ peut être vide dans les formulaires
  • auto_now_add et auto_now utiles 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 de app_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 :

  • widgets pour contrôler le HTML
  • labels et help_texts pour 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é serveur
  • messages.success pour 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=book pour lier le formulaire à l’objet existant
  • On réutilise le même template book_form.html pour 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 :

  1. Seules les personnes authentifiées peuvent créer / modifier / supprimer.
  2. 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 → liste
  • DetailView → détail
  • CreateView → création
  • UpdateView → mise à jour
  • DeleteView → 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