
Un projet exemple "blog" est parfait pour illustrer les vues génériques :
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Author(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
def __str__(self):
return self.user.username
class Meta:
verbose_name = 'Auteur'
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='posts')
def __str__(self):
return self.title
class Meta:
ordering = ['-created_at']
verbose_name = 'Article'
Voici les imports qui seront utilisés dans l'article :
from .models import Post
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.core.exceptions import PermissionDenied
from django.urls import reverse_lazy, reverse
ListView : Afficher une liste d'objets
La ListView permet de récupérer une liste d'objets :
from .models import Post
from django.views.generic import ListView
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
# urls.py
path('', PostListView.as_view(), name='home')
# Je m'en sers comme page d'index
Il suffit de définir le modèle utilisé et le template. Je préfère redéfinir le contexte, qui est par défaut object_list.
<h1>Blog Posts</h1>
<ul>
{% for post in posts %}
<li>
<a href="#">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
On peut même implémenter la pagination en une ligne ? :
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
paginate_by = 10
Par défaut, le queryset est Model.objects.all(), mais on pourrait très bien le modifier avec la méthode get_queryset :
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(...)
Et pourquoi ne pas ajouter du contexte ?
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
paginate_by = 10
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['autre'] = 'Génial mon contexte !'
return context
Comme vous pouvez le voir, rien qu'avec la ListView, les vues génériques sont très flexibles.
Nous allons nous pencher sur cet exemple pour voir comment on peut restreindre l'accès à notre vue.
Différentes façons de restreindre l'accès aux vues
Avec des mixins
Il est possible d'utiliser le LoginRequiredMixin pour obliger l'utilisateur à être connecté :
# Autres imports ...
from django.contrib.auth.mixins import LoginRequiredMixin
class PostListView(LoginRequiredMixin, ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
paginate_by = 10
# def get_queryset(self):
# queryset = super().get_queryset()
# return queryset.filter(...)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['autre'] = 'Génial mon contexte !'
return context
On peut aussi utiliser les permissions de Django :
# Autres imports ...
from django.contrib.auth.mixins import PermissionRequiredMixin
class PostListView(PermissionRequiredMixin, ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
paginate_by = 10
permission_required = 'blog.view_post'
# def get_queryset(self):
# queryset = super().get_queryset()
# return queryset.filter(...)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['autre'] = 'Génial mon contexte !'
return context
On peut spécifier la ou les permissions grâce à l'attribut permission_required. Si on le souhaite, le PermissionRequiredMixin nous donne la possibilité de surcharger la méthode has_permission :
class PostListView(PermissionRequiredMixin, ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
paginate_by = 10
# def get_queryset(self):
# queryset = super().get_queryset()
# return queryset.filter(...)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['autre'] = 'Génial mon contexte !'
return context
def has_permission(self):
return self.request.user.has_perm('blog.view_post') or self.request.user.is_superuser
Avec un décorateur
Vous pouvez aussi décorer la classe elle-même :
# Autres imports ...
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
@method_decorator(login_required, name='dispatch')
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
paginate_by = 10
# def get_queryset(self):
# queryset = super().get_queryset()
# return queryset.filter(...)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['autre'] = 'Génial mon contexte !'
return context
Dans ce cas précis, on cible directement la méthode dispatch avec le décorateur login_required, décorateur que l'on retrouve assez souvent avec des vues basées sur des fonctions.
La méthode dispatch ?
Surcharger la méthode dispatch
On a vu comment cibler la méthode dispatch en décorant la classe elle-même. Mais il est possible de gérer l'accès aux vues en surchargeant cette méthode. Bien que je préfère les solutions précédentes, voyons comment on peut faire cela :
# Autres imports ...
from django.core.exceptions import PermissionDenied
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts' # 'object_list' par défaut
paginate_by = 10
# def get_queryset(self):
# queryset = super().get_queryset()
# return queryset.filter(...)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['autre'] = 'Génial mon contexte !'
return context
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
raise PermissionDenied("Vous devez être un administrateur pour voir cette page.")
return super().dispatch(request, *args, **kwargs)
Les concepts d'authentification et de permissions vus ci-dessus s'appliquent à toutes les CBV Django.
DetailView : afficher une instance
La DetailView permet de récupérer une instance de modèle.
Voici un exemple typique :
# Autres imports (notamment Post)
from django.views.generic import DetailView
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
Comme avec la ListView, je préfère redéfinir le contexte pour afficher mon instance, qui par défaut est object.
Le reste est basique, on définit le model et le template.
Django cherchera le pk ou le slug ( en fonction de ce que vous allez définir dans l'url) pour afficher l'instance :
from django.urls import path
from .views import PostDetailView
app_name = 'blog'
urlpatterns = [
path('<int:pk>/', PostDetailView.as_view(), name='post_detail'),
]
# Si on utilise le slug :
# path('post/<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
Vous pouvez modifier le contexte de la même manière que la ListView. Pourquoi modifier le queryset pour récupérer notre objet ?
Voici un exemple d'optimisation :
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return super().get_queryset().select_related('author')
select_related ? Je vous invite à regarder ma courte vidéo Youtube sur le sujet ?.
À ce stade, il peut être intéressant de voir un exemple du code HTML pour les deux premières vues :
<!-- post_list.html -->
<h1>Blog Posts</h1>
{{ autre }} <!-- Le contexte ajouté avec def get_context_data -->
<ul>
{% for post in posts %}
<li>
<a href="{% url 'blog:post_detail' post.pk %}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">« first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
{% endif %}
</span>
</div>
<!-- post_detail.html -->
<h1>{{ post.title }}</h1>
<p>Par {{ post.author.user.username }} le {{ post.created_at|date:"d/m/Y" }}</p>
<div>{{ post.content }}</div>
La CreateView : création d'objet simplifiée !
La CreateView permet de générer une vue avec formulaire pour créer une instance de modèle.
Commençons avec quelques attributs et la méthode form_valid :
# Autres imports (notamment Post)
from django.views.generic import CreateView
from django.urls import reverse_lazy, reverse
class PostCreateView(CreateView):
model = Post
template_name = 'blog/post_form.html'
fields = ['title', 'content']
# form_class = CustomForm # Si vous avez un formulaire personnalisé dans forms.py
success_url = reverse_lazy('home') # Redirection après la création
def form_valid(self, form):
form.instance.author = self.request.user.author
return super().form_valid(form)
# urls.py
path('create/', PostCreateView.as_view(), name='post_create')
<h1>Créer un nouvel article</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Créer</button>
</form>
Deux nouveaux attributs : fields et success_url. fields permet d'afficher les champs souhaités dans le formulaire, tandis que success_url détermine l'url de redirection en cas de succès.
form_class (commenté dans ce cas) permet d'utiliser un formulaire personnalisé importé depuis forms.py.
La méthode form_valid est pratique pour réaliser des actions sur la validation du formulaire : je n'affiche pas l'auteur dans le formulaire, mais le champ author sera automatiquement renseigné en fonction de l'user qui valide le formulaire.
Mais on peut aller plus loin avec la méthode form_valid :
- Récupérer des éléments passés en paramètres d'URL (via les kwargs)
- Récupérer des champs du formulaire
class PostCreateView(CreateView):
model = Post
template_name = 'blog/post_form.html'
fields = ['title', 'content']
# form_class = CustomForm # Si vous avez un formulaire personnalisé dans forms.py
# success_url = reverse_lazy('home') # Redirection après la création
def form_valid(self, form):
# foo_in_kwargs = self.kwargs.get('foo', None) # Récupération d'un paramètre dans l'URL
title = form.cleaned_data.get('title')
form.instance.title = title.upper()
form.instance.author = self.request.user.author
return super().form_valid(form)
def get_success_url(self): # Redirection après la création
return reverse('blog:post_detail', args=[self.object.pk])
Contrairement à l'attribut success_url, la méthode get_success_url permet de passer des paramètres à une URL, et de créer une logique conditionnelle pour la redirection.
Je me répète, mais... on voit bien à quel point les vues génériques sont très flexibles.
L'UpdateView : modification d'un objet
Je ne vais pas passer trop de temps dessus, car tout comme la CreateView, on peut utiliser la méthode form_valid pour manipuler les données.
Il faut penser à passer le pk de l'objet en paramètre d'URL.
# Autres imports (notamment Post)
from django.views.generic import UpdateView
class PostUpdateView(UpdateView):
model = Post
template_name = 'blog/post_update_form.html'
fields = ['title', 'content']
def get_success_url(self):
return reverse('blog:post_detail', args=[self.object.pk])
# urls.py
path('<int:pk>/update/', PostUpdateView.as_view(), name='post_update')
<h1>Modifier l'article : {{ form.instance.title }}</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Modifier</button>
</form>
Le HTML est tellement similaire entre la CreateView et l'UpdateView qu'on pourrait facilement utiliser le même template pour les deux.
La DeleteView : suppression d'un objet
La DeleteView permet de supprimer une instance de modèle :
from django.urls import reverse_lazy
from django.views.generic import DeleteView
class PostDeleteView(DeleteView):
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('home')
Il faut donc penser à passer le pk en paramètre :
# urls.py
path('<int:pk>/delete/', PostDeleteView.as_view(), name='post_delete')
Qu'il s'agisse de l'UpdateView ou de la DeleteView, pensez à passer le pk dans vos liens :
<!-- post_detail.html -->
<h1>{{ post.title }}</h1>
<p>Par {{ post.author.user.username }} le {{ post.created_at|date:"d/m/Y" }}</p>
<div>{{ post.content }}</div>
<a href="{% url 'blog:post_update' post.pk %}">Modifier</a>
<a href="{% url 'blog:post_delete' post.pk %}">Supprimer</a>
Pour la DeleteView, le template est utilisé comme page de confirmation :
<!-- post_confirm_delete.html -->
<h1>Confirmer la suppression de l'article : {{ post.title }}</h1>
<form method="post">
{% csrf_token %}
<p>Êtes-vous sûr de vouloir supprimer cet article ?</p>
<button type="submit">Supprimer</button>
</form>
Quelques mots pour conclure
Restreindre l'accès à vos vues
En début d'article (pour la ListView), j'ai montré comment restreindre l'accès à vos vues. Même si je ne l'ai pas fait pour les autres exemples, pensez à bien protéger toutes vos vues sensibles.
La méthode setup
Dans mes vues génériques, il m'arrive parfois d'avoir besoin d'utiliser un objet à plusieurs endroits.
Dans l'exemple suivant (qui n'a rien à voir avec nos exemples précédent, car il s'agit d'un projet personnel), je récupère le pk d'un objet parent via l'url avec les kwargs. Je crée un attribut d'instance self.project, que je réutilise dans plusieurs méthodes.
En réalité, la méthode setup est appelée juste après la méthode __init__ avec les vues génériques.
# Nouveaux imports
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import get_object_or_404
# Autres imports ...
@method_decorator(staff_member_required, name="dispatch")
class CreateService(CreateView):
model = Service
template_name = "customers/service_form.html"
form_class = ServiceForm
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.project = get_object_or_404(Project, pk=kwargs.get("pk"))
def form_valid(self, form):
form.instance.project = self.project
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["status"] = "Création d'un service"
context["project"] = self.project
return context
def get_success_url(self):
return reverse("customers:project", kwargs={"pk": self.project.pk})
Retour