Créer un blog avec Django

Découvre comment créer un blog avec le framework Django.

Publié le par Thibault Houdon (mis à jour le )
paceTemps de lecture estimé : 107 minutes

Dans ce (très) long article, on va voir comment créer un blog avec Django !

Pas un blog complet avec des commentaires, plein d'utilisateurs et une mise en page à couper le souffle.

Mais un blog tout de même 100% fonctionnel, avec la possibilité de créer des articles, de les afficher, les modifier et les supprimer, tout ça grâce notamment aux vues fondées sur les classes (Class Based Views) de Django.

L'objectif de cet article, malgré sa longueur, est de te montrer à quel point Django permet de gérer énormément de choses sur un projet à partir de parfois 2 ou 3 lignes de code.

Pour ne pas être complètement perdu à la lecture de cet article, il est nécessaire de connaître les bases de Python et d'avoir déjà au moins manipulé un peu le framework Django. Pas besoin d'être un expert, j'explique tout dans les détails, mais avoir déjà réalisé au moins un projet avec Django vous permettra de ne pas être perdu à la lecture de cet article.

Vous pouvez retrouver toutes les sources de cet article sur le dépôt Github de Docstring.

Voici un aperçu du site que nous allons réaliser (avec une mise en forme CSS que nous ne verrons pas dans cet article) :

La page d'accueil du blog

Formulaire pour ajouter un article

Création du projet Django

La première chose à faire est de créer un dossier et un environnement virtuel pour notre projet de blog.

Dans un terminal, créez un dossier et naviguez dedans :

mkdir django_blog && cd django_blog

Une fois à l'intérieur du dossier, créez un environnement virtuel avec le module venv :

python3.9 -m venv .env

Une fois l'environnement virtuel créé, vous pouvez l'activer et installer le framework Django :

source .env/bin/activate
pip install Django==3.1.7

Dans cette formation, nous allons également utiliser une base de données PostgreSQL, il faut donc également installer le package psycopg2 :

pip install psycopg2==2.8.6

Ensuite, nous allons créer notre projet Django grâce à la commande django-admin:

django-admin startproject blog

Personnellement, j'aime bien renommer le dossier racine créé par Django par src. Je peux le faire directement depuis le terminal avec la commande mv (move) :

mv blog src

À ce stade, j'ai donc la structure de dossier suivante :

Création et configuration de la base de données

Pour cette formation, nous utilisons une base de données PostgreSQL. Si vous n'êtes pas très à l'aise avec Postgres, vous pouvez très bien passer cette partie et continuer avec la base de données sqlite3 par défaut.

Pour pouvoir utiliser PostgreSQL, il faut au préalable l'installer pour votre système d'exploitation.

Dans un terminal, vous pouvez ouvrir un shell PostgreSQL avec la commande psql (sur Windows, utilisez le SQL Shell).

Une fois à l'intérieur du shell, nous allons créer une base de données blog pour le projet avec la commande create database :

CREATE DATABASE blog;

Nous allons ensuite créer un utilisateur qui aura accès à cette base de données. Nous faisons ceci pour des raisons de sécurité, l'utilisateur que nous allons créer n'ayant accès qu'à la base de données de notre projet et pas aux autres bases de données que vous pourriez avoir sur votre serveur PostgreSQL.

Pour ça, nous utilisons la commande create user :

CREATE USER blogadmin WITH ENCRYPTED PASSWORD '123456';
ALTER ROLE blogadmin SET client_encoding TO 'utf8';
ALTER ROLE blogadmin SET default_transaction_isolation TO 'read committed';

Pour finir, il ne nous reste plus qu'à donner accès à notre utilisateur à la base de données créée précédemment :

GRANT ALL PRIVILEGES ON DATABASE blog TO blogadmin;

Maintenant que nous en avons terminé avec la configuration de notre base de données, il est temps d'aller indiquer à notre projet Django que c'est cette base de données qu'il devra utiliser.

Ouvrez le fichier settings.py et trouvez la variable DATABASES, qui pour l'instant doit ressembler à ceci :

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

Nous allons modifier la clé 'ENGINE' pour indiquer que nous souhaitons utiliser postgresql et rajouter quelques variables pour indiquer la base de donnée et l'utilisateur que nous souhaitons utiliser :

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'blog',
        'USER': 'blogadmin',
        'PASSWORD': '123456',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

La clé 'NAME' correspond au nom de la base de données que nous avons créé.

Les clés 'USER' et 'PASSWORD' correspondent au nom d'utilisateur et au mot de passe de l'utilisateur blogadmin créé ci-dessus.

La clé 'HOST' contient 'localhost', qui est un raccourci pour l'adresse IP locale de votre ordinateur (127.0.0.1).

Pour le port, si vous utilisez le port par défaut de PostgreSQL (5432), vous n'êtes pas obligé de l'indiquer (mais comme pour Python, il est toujours préférable d'être explicite plutôt qu'implicite, raison pour laquelle je préfère l'indiquer quand même).

Et voilà, notre projet est maintenant correctement créé et configuré avec PostgreSQL et nous pouvons rentrer dans le vif du sujet.

Création de l'applicationNous allons maintenant créer une application posts pour gérer les articles de notre blog.

À l'intérieur du dossier src, dans lequel se trouve le fichier manage.py, nous allons utiliser la commande startapp pour créer l'application.

Assurez-vous d'avoir bien activé votre environnement virtuel auparavant.

python manage.py startapp posts

Une fois l'application créée, il ne nous reste plus qu'à l'ajouter dans le fichier settings.py à la liste des applications de notre projet, contenues dans la variable INSTALLED_APPS :

# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'posts'  # On rajoute le nom de l'application
]

Création du modèle pour les articlesDans le fichier models.py de notre application posts, nous allons créer un modèle pour sauvegarder nos articles dans la base de données.

# posts/models.py
User = get_user_model()


class BlogPost(models.Model):
    title = models.CharField(max_length=255, unique=True, verbose_name="Titre")
    slug = models.SlugField(max_length=255, unique=True, blank=True)
    author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
    last_updated = models.DateTimeField(auto_now=True)
    created_on = models.DateField(blank=True, null=True)
    published = models.BooleanField(default=False, verbose_name="Publié")
    content = models.TextField(blank=True, verbose_name="Contenu")

Quelques éléments à noter ici :

  • Pour récupérer le modèle utilisateur, on utilise la fonction get_user_model. Cette fonction va récupérer la valeur de la variable settings.AUTH_USER_MODEL. On pourrait utiliser directement cette variable, les deux fonctionnent légèrement différemment, mais il serait trop long d'expliquer ces différences ici.
  • On utilise les paramètres verbose_name directement sur nos modèles. Cela permettra d'afficher les labels de nos formulaires avec des mots français quand nous créerons les vues. Il existe de nombreuses façons de modifier l'affichage d'un formulaire (et de traduire les termes, notamment avec i18). Le paramètre verbose_name est un moyen simple et efficace de modifier le nom affiché dans les différentes interfaces (formulaires, interface d'administration...).
  • Pour le champ author, on utilise une clé étrangère avec le paramètre on_delete à SET_NULL. Vous trouverez de nombreux tutoriels qui mettent par défaut models.CASCADE. Il est bien important de comprendre l'importance de ce paramètre. Avec models.CASCADE, cela signifie que si vous supprimez l'utilisateur relié à vos articles, les articles seront également supprimés ! Êtes-vous sûr que c'est vraiment le comportement souhaité ? models.CASCADE est très pratique, mais il faut réellement comprendre ce que l'on fait. Je connais des projets qui ont perdu beaucoup de données importantes, car les développeurs mettaient models.CASCADE dans toutes les relations des clés étrangères de leurs modèles.
  • Pour le champ last_updated, on passe le paramètre auto_now à True. Le champ sera mis à jour automatiquement à chaque modification de l'article dans la base de données. Pratique 👌

Nous allons rajouter quelques informations sur le modèle, notamment une classe Meta qui va nous permettre de modifier l'ordre d'affichage par défaut des articles dans l'interface d'administration ainsi que le nom affiché (par défaut, l'interface afficherait le nom du modèle, donc BlogPost) :

# posts/models.py
class BlogPost(models.Model):
    title = models.CharField(max_length=255, unique=True, verbose_name="Titre")
    slug = models.SlugField(max_length=255, unique=True, blank=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
    last_updated = models.DateTimeField(auto_now=True)
    created_on = models.DateField(blank=True)
    published = models.BooleanField(default=False, verbose_name="Publié")
    content = models.TextField(blank=True, verbose_name="Contenu")

    # On rajoute une classe Meta pour préciser notre modèle
    class Meta:
        ordering = ['-created_on']
        verbose_name = "Article"

Nous allons également rajouter la méthode __str__ pour utiliser le titre des articles :

def __str__(self):
    return self.title

Pour finir, j'aime bien surcharger la méthode save afin de rajouter le slug automatiquement en fonction du titre de l'article.

Pour ce faire, nous allons utiliser la fonction slugify du module django.template.defaultfilters :

def save(self, *args, **kwargs):
    if not self.slug:
        self.slug = slugify(self.title)

    super().save(*args, **kwargs)

Si aucun slug n'est indiqué par l'auteur de l'article, on utilise slugify sur le titre de l'article pour en générer un automatiquement 😉

Créer les migrations dans la base de données. Maintenant que notre modèle est créé, nous allons pouvoir créer les migrations et les appliquer pour créer les tables dans la base de données de notre projet.

Nous allons ainsi également appliquer les migrations de base de Django qui vont créer les tables pour la gestion des utilisateurs.

Dans un terminal (assurez-vous de sourcer votre environnement virtuel au préalable), nous allons donc utiliser les deux commandes suivantes :

python manage.py makemigrations
python manage.py migrate

Votre terminal devrait afficher quelque chose de similaire :

$ python manage.py makemigrations
Migrations for 'posts':
  posts/migrations/0001_initial.py
    - Create model BlogPost

$ python manage.py migrate       
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, posts, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying posts.0001_initial... OK
  Applying sessions.0001_initial... OK

Si à ce stade vous avez des erreurs, vous avez peut-être oublié d'activer votre environnement virtuel ou vous avez mal effectué une des opérations ci-dessus. Il ne vous reste plus qu'à pleurer et à retourner au début de l'article pour tout refaire.

Ajouter le modèle dans l'interface d'administrationAvant d'ajouter le modèle, nous allons déjà créer un utilisateur admin pour pouvoir nous connecter à l'interface d'administration de Django. C'est également avec cet utilisateur que nous signerons les articles (dans ce projet, nous ne gérons pas la possibilité d'ajouter plusieurs auteurs. Le seul auteur des articles sera donc l'utilisateur admin).

Pour ça, nous allons utiliser la commande createsuperuser :

$ python manage.py createsuperuser
Username (leave blank to use 'thibh'): thibh
Email address: hello@docstring.fr
Password: 
Password (again): 
Superuser created successfully.

Maintenant que nous avons un utilisateur, vous pouvez vous connecter à votre interface d'administration avec celui-ci.

Lancez votre serveur avec la commande runserver (python manage.py runserver) et rendez-vous à l'adresse http://127.0.0.1:8080/admin/.

Cependant, pour l'instant, vous ne verrez pas vos articles. Pas possible donc d'ajouter des articles de test.

Pour ça, nous devons enregistrer les modèles que nous souhaitons afficher dans le fichier admin.py présent dans le dossier de notre application posts :

# posts/admin.py
from django.contrib import admin

from posts.models import BlogPost


class BlogPostAdmin(admin.ModelAdmin):
    list_display = ("title", "published", "created_on", "last_updated")
    list_editable = ("published",)


admin.site.register(BlogPost, BlogPostAdmin)

list_display et list_editable nous permettent de spécifier les champs de notre modèle que nous souhaitons afficher et ceux qu'il sera possible d'éditer directement depuis la vue de l'interface d'administration contenant tous les articles.

Si vous vous rendez dans votre interface d'administration, vous devriez maintenant avoir la possibilité d'ajouter un article, de les modifier, les supprimer et visualiser les articles de votre base de données.

Si vous souhaitez changer la langue de l'interface d'administration de Django, il suffit de modifier la variable LANGUAGE_CODE dans le fichier settings.py. Par défaut, la langue est en anglais ('en-us'). Il suffit de mettre à la place 'fr-fr' (ou 'fr-br' si vous mangez des crêpes au petit déjeuner) :

# settings.py
LANGUAGE_CODE = 'fr-fr'

Voici ce que vous devriez avoir dans l'interface d'administration :

Créer le template HTML de base

Nous allons maintenant créer un fichier HTML de base que nous étendrons par la suite avec les différentes classes qui afficheront les vues de notre application.

Dans le dossier src, nous allons créer un dossier templates qui contiendra les fichiers HTML communs à notre projet et non pas à une application en particulier.

Puis à l'intérieur de ce dossier, nous créons un fichier base.html.

Pour que Django puisse trouver ce template, nous devons ajouter le dossier templates dans le fichier settings.py.

Localisez la variable TEMPLATES et ajoutez dans la liste correspondant à la clé 'DIRS' le chemin BASE_DIR / 'templates' :

# settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  # On ajoute le dossier templates
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Avec les anciennes versions de Django, il fallait utiliser le module os et la fonction os.path.join pour créer des chemins de dossier. Dans les versions récentes de Django, la bibliothèque pathlib est utilisée pour les variables telles que BASE_DIR.

On peut donc concaténer rapidement un chemin avec le slash (/) comme nous le faisons ci-dessus.

À l'intérieur du fichier base.html, nous insérons les balises HTML qui seront communes à toutes nos pages, ainsi que deux blocs dans lesquels nous insérerons des données par la suite (un bloc title pour le titre de la page et un bloc content pour le contenu) :

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    {% block title %}{% endblock %}
</head>
<body>

<section id="blog">
    {% block content %}{% endblock %}
</section>

</body>
</html>

Création de la vue d'accueil du blog

Dans cette partie, nous allons créer la page d'accueil du blog.

L'objectif ici est de récupérer tous les articles publiés et de les afficher les uns à la suite des autres en ordre de publication.

C'est précisément le genre de situation pour laquelle la vue fondée sur une classe ListView est utilisée.

Dans la suite de cet article, j'utiliserai l'acronyme CBV (Class Based View) pour parler des vues fondées sur les classes. Dans le fichier views.py de notre application posts, nous allons donc créer une vue qui hérite de ListView et qui utilise notre modèle d'article :

# posts/views.py
from django.views.generic import ListView

from posts.models import BlogPost

class BlogHome(ListView):
    model = BlogPost
    context_object_name = "posts"

L'attribut context_object_name permet de spécifier un nom que l'on souhaite utiliser pour accéder aux données de notre queryset (dans ce cas les articles du blog). Par défaut, ListView permet d'utiliser le nom object_list ou le nom du modèle en minuscule suivi de _list (donc dans notre cas blogpost_list).

Si vous définissez l'attribut context_object_name, vous pourrez toujours utiliser object_list pour accéder aux éléments du queryset mais vous ne pourrez plus utiliser blogpost_list!

Nous allons ensuite créer un fichier urls.py à l'intérieur de l'application posts pour ajouter des chemins d'URLs qui utiliseront notre CBV.

Nous allons créer un chemin d'URL à la racine de notre application (représenté par la chaîne de caractères vide) et renvoyer ce chemin vers notre vue BlogHome. Pour pouvoir utiliser notre CBV dans le chemin d'URL, il ne faut pas oublier d'utiliser la méthode as_view :

# posts/urls.py
from django.urls import path
from posts.views import BlogHome

app_name = "posts"

urlpatterns = [
    path('', BlogHome.as_view(), name="home")
]

La variable app_name permet d'utiliser un espace de nommage pour notre application lors de l'utilisation de la balise de gabarit url. Par exemple, si nous souhaitons accéder à la page d'accueil du blog, nous pourrons utiliser la balise suivante : {% url 'posts:home' %}.

Pour pouvoir accéder à cette url, il faut inclure notre fichier d'URL dans la configuration d'URL principale de notre projet.

Django ne dispose en effet que d'un seul point d'entrée pour résoudre les URL. Ce point d'entrée est défini dans le fichier settings.py (encore lui !) dans la variable ROOT_URLCONF.

Par défaut, cette variable pointe vers le fichier urls.py de votre projet Django (donc ici dans le dossierblog).

Nous allons donc utiliser la fonction include pour inclure les urls de notre application posts à l'intérieur du fichier principal d'urls :

# blog/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('posts.urls'))  # On inclue les chemins d'URLs de l'application posts
]

Nous pouvons maintenant essayer d'accéder à la page d'accueil de notre blog en lançant le serveur et en nous rendant à l'adresse URL reliée à notre CBV BlogHome.

Dans un terminal, lancez le serveur de développement avec :

python manage.py runserver

Et rendez-vous à l'adresse http://127.0.0.1:8080/blog/

Si tout se passe bien, vous devriez tomber sur une belle erreur vous indiquant que le template pour la vue n'est pas trouvé :

TemplateDoesNotExist at /blog/

Par défaut, les CBV de type ListView vont chercher un template placé dans le dossier templates de l'application et nommé nomdumodele_list.html.

Dans le cas de notre modèle BlogPost, avec une vue contenue à l'intérieur de l'application posts, Django va donc chercher un template situé dans src/posts/templates/posts/blogpost_list.html.

Nous allons donc créer ce fichier et inclure le code HTML suivant :

<!-- posts/templates/posts/blogpost_list.html -->
{% extends 'base.html' %}

{% block title %}
    <title>Accueil du blog</title>
{% endblock %}

{% block content %}

    <h1>Le blog de Docstring</h1>
    {% for post in posts %}
        <article>
            <h2>{{ post.title }}</h2>
            <h5>Publié par <i>{{ post.author.username }}</i> le {{ post.created_on|date:'j F Y' }}</h5>
            <p>{{ post.content|safe|truncatewords:50 }}</p>
        </article>
    {% endfor %}

{% endblock %}

Tout d'abord, on utilise la balise extends pour étendre le fichier base.html créé plus haut.

On utilise ensuite la balise block pour insérer du code HTML à l'intérieur du block content.

Nous pouvons boucler sur tous les articles du blog grâce à la variable posts (souvenez-vous, c'est la valeur que nous avons donné à l'attribut context_object_name dans notre classe BlogHome). On aurait pu utiliser également object_list.

À l'intérieur de la boucle, nous incluons tous les éléments dont nous avons besoin pour nos articles (que l'on entoure sémantiquement avec la balise HTML {}).

post.author.username nous permet d'accéder au nom d'utilisateur de la clé étrangère correspondant au champ author. Il faut noter que le template ne retournera pas d'erreur dans le cas ou un article n'est pas associé à un auteur. On pourrait créer une propriété sur notre modèle d'article pour afficher un texte par défaut dans le cas ou un article n'a pas d'auteur associé :

# posts/models.py

class BlogPost(models.Model):
    title = models.CharField(max_length=255, unique=True, verbose_name="Titre")
    slug = models.SlugField(max_length=255, unique=True, blank=True)
    author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
    last_updated = models.DateTimeField(auto_now=True)
    created_on = models.DateField(blank=True)
    published = models.BooleanField(default=False, verbose_name="Publié")
    content = models.TextField(blank=True, verbose_name="Contenu")

    @property
    def author_or_default(self):
        return self.author.username if self.author else "L'auteur inconnu"

On peut ensuite utiliser cette propriété dans notre template HTML :

<h5>Publié par <i>{{ post.author_or_default }}</i></h5>

Pour afficher la date, on utilise le filtre date.

Pour voir les options de formatage disponible, je vous renvoie à la documentation qui explique tout en détail :

{{ post.created_on|date:'j F Y' }}

Si vous avez bien modifié la langue dans le fichier settings.py, la date sera automatiquement affichée en français, sous le format suivant : Publié par thibh le 20 mars 2021.

Pour finir, nous utilisons les filtres safe et truncatewords pour afficher un extrait (les 50 premiers mots) du contenu des articles :

{{ post.content|safe|truncatewords:50 }}

Une fois que vous avez créé tous les fichiers HTML, je vous recommande d'arrêter et de redémarrer votre serveur (avec la commande runserver) pour que Django puisse prendre en compte tous les changements.

Quand on ajoute des templates, il est souvent nécessaire de donner un petit coup de coude au serveur en le relançant 😉

Voici à quoi ressemble pour l'instant notre page d'accueil :

Dernière chose avant de passer à la suite : actuellement, nous affichons tous les articles, qu'ils soient publiés ou non.

Idéalement, on aimerait pouvoir distinguer deux cas de figure :

  • Pour les visiteurs de notre site → afficher uniquement les articles publiés.
  • Pour l'administrateur du site → afficher tous les articles.

Malheureusement, Django ne nous permet pas de gérer ces cas de figure.

...

Vous m'avez cru ? Mais non ! Django nous permet bien entendu de gérer ces deux cas de figure. Qu'est-ce que vous croyez, Python n'est pas le meilleur langage du monde sans raison (bon, en fait il est 2e).

Pour modifier les articles affichés en fonction de l'utilisateur connecté, nous allons utiliser la méthode get_queryset, disponible sur les ListView.

Cette méthode nous permet de modifier le queryset, c'est-à-dire les entrées de notre base de données qui seront passées à la vue :

# posts/views.py

class BlogHome(ListView):
    model = BlogPost
    context_object_name = "posts"

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.request.user.is_authenticated:
            return queryset

        return queryset.filter(published=True)

Tout d'abord, on récupère le queryset d'origine en utilisant la fonction super et en appelant la fonction get_queryset de la classe ListView :

queryset = super().get_queryset()

On vérifie ensuite si l'utilisateur est connecté (on pourrait également vérifier si l'utilisateur est bien administrateur avec la propriété is_superuser, mais dans ce cas-ci, nous n'avons qu'un utilisateur et c'est l'administrateur du site) :

if self.request.user.is_authenticated:
    return queryset

Si l'utilisateur est connecté, on retourne le queryset tel quel.

Si l'utilisateur n'est pas connecté, on continue, et on filtre le queryset pour ne récupérer que les articles dont le champ published contient la valeur True (les articles publiés, donc) :

return queryset.filter(published=True)

Créer la vue pour ajouter un article

Nous allons maintenant créer une vue qui va nous permettre d'ajouter un article.

Pour les prochaines parties, la procédure va être assez similaire :

  1. Création d'une CBV.
  2. Création d'un chemin d'URL qui pointe vers la CBV.
  3. Création d'un fichier HTML pour afficher les données passées par la CBV.

Commençons donc par créer une classe qui hérite de CreateView :

# posts/views.py
from django.views.generic import CreateView

from posts.models import BlogPost

class BlogPostCreate(CreateView):
    model = BlogPost
    template_name = 'posts/blogpost_create.html'
    fields = ['title', 'content', ]

Là encore je renseigne le modèle BlogPost dans l'attribut model.

Par défaut, les vues de type CreateView vont aller chercher un template dans applicationame/modelname_form.html.

Dans notre cas, notre classe BlogPostCreate va donc chercher le template posts/blogpost_form.html.

Comme je suis un peu rebelle, je décide de modifier le template à utiliser en le spécifiant dans l'attribut template_name.

Je trouve en effet le terme "form" trop générique et je souhaite indiquer que ce fichier HTML sera spécifiquement utilisé pour la création d'un article.

Dans l'attribut fields, j'indique les deux champs de mon modèle que je souhaite afficher dans mon formulaire HTML.

Créons maintenant le fichier posts/blogpost_create.html :

{% extends 'base.html' %}

{% block title %}
    <title>Ajouter un article</title>
{% endblock %}

{% block content %}

    <h1>Ajouter un article</h1>

    <form method="POST"></form>
        {% csrf_token %}
        {{ form }}
        <input type="submit" value="Créer">
    </form>

{% endblock %}

Plusieurs choses à noter ici.

Tout d'abord, on modifie le titre de la page avec le block title.

On ajoute ensuite un formulaire avec la méthode POST.

Faites bien attention de ne pas oublier le csrf_token avec la balise {% csrf_token %} !

On ajoute ensuite notre formulaire qui est disponible avec la variable form. C'est le nom par défaut qui est donné à notre formulaire par la classe CreateView.

Pour finir, on ajoute un input de type submit (qui correspond à un bouton HTML) pour nous permettre d'envoyer le formulaire et ainsi créer notre article.

Il ne nous reste plus qu'à relier notre CBV à une URL dans le fichier urls.py de notre application posts :

# posts/urls.py

from django.urls import path
from .views import BlogHome, BlogPostCreate

app_name = "posts"

urlpatterns = [
    path('', BlogHome.as_view(), name="home"),
    path('create/', BlogPostCreate.as_view(), name="create")  # On ajoute ce chemin d'URL
]

Nous pouvons maintenant aller tester notre formulaire en lançant le serveur (avec la commande runserver, vous connaissez la chanson) et en nous rendant à l'adresse http://127.0.0.1:8080/blog/create/ :

Ce formulaire est vraiment magnifique (🫣)

Nous pouvons maintenant ajouter un article directement depuis notre site, sans passer par l'interface d'administration !

Cependant si vous essayez de créer un article... patatra !

Une erreur nous indique No URL to redirect to. Either provide a url or define a get_absolute_url method on the Model.

Nous avons deux possibilités : indiquer une URL vers laquelle rediriger dans notre vue BlogPostCreate ou créer une méthode get_absolute_url sur le modèle.

Nous allons partir sur la 2e solution, puisque la méthode get_absolute_url est utilisé par Django à différents endroits et nous permet d'obtenir l'URL unique pour accéder à un article.

Dans notre modèle BlogPost, nous allons donc ajouter cette méthode :

# posts/models.py
from django.urls import reverse

User = get_user_model()


class BlogPost(models.Model):
    title = models.CharField(max_length=255, unique=True, verbose_name="Titre")
    slug = models.SlugField(max_length=255, unique=True, blank=True)
    author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
    last_updated = models.DateTimeField(auto_now=True)
    created_on = models.DateField(blank=True, null=True)
    published = models.BooleanField(default=False, verbose_name="Publié")
    content = models.TextField(blank=True, verbose_name="Contenu")

    # On ajoute la méthode get_absolute_url
    def get_absolute_url(self):
        return reverse("posts:home")

Pour l'instant, nous n'avons pas encore créé de vue pour accéder à un article individuel, nous retournons donc tout simplement la vue d'accueil du blog.

Vous remarquez ici que j'utilise le nom que j'ai donné à app_name dans le fichier urls.py de mon application posts. C'est le fameux espace de nom qui nous permet de spécifier à quelle application appartient l'URL. On pourrait ainsi avoir plusieurs URL nommées 'home' et éviter les conflits grâce à l'espace de nom de l'application.

Si vous essayez de recréer un article, cette fois-ci cela devrait fonctionner et vous serez redirigé vers la page d'accueil du blog 🥳

Créer la vue pour modifier un article

Bon, vous commencez à être habitué, donc je vais aller un peu plus vite pour cette partie vu qu'il s'agit presque de la même chose que pour la vue de création.

On crée donc une vue à partir de UpdateView pour modifier notre article :

# posts/views.py

from django.views.generic import UpdateView

class BlogPostEdit(UpdateView):
    model = BlogPost
    template_name = 'posts/blogpost_edit.html'
    fields = ['title', 'content', 'published', ]

Là encore, mon esprit rebelle me fait changer le template pour posts/blogpost_edit.html que je trouve plus explicite.

Pour les champs, on a ajouté le champ published car on souhaite pouvoir modifier le statut de publication de l'article.

On rajoute ensuite une URL dans le fichier posts/urls.py qui nous permet d'accéder à cette vue :

# posts/urls.py

from django.urls import path
from .views import BlogHome, BlogPostCreate, BlogPostEdit

app_name = "posts"

urlpatterns = [
    path('', BlogHome.as_view(), name="home"),
    path('create/', BlogPostCreate.as_view(), name="create"),
    path('edit//', BlogPostEdit.as_view(), name="edit"),
]

Afin de pouvoir récupérer l'article que l'on souhaite éditer, nous devons indiquer un emplacement dans l'URL pour récupérer le slug, ce que l'on fait avec <str:slug>.

Il ne nous reste plus qu'à créer le fichier blogpost_edit.html dans lequel nous allons inclure du code HTML quasi-identique au fichier blogpost_create.html :

<!-- posts/templates/posts/blogpost_edit.html -->
{% extends 'base.html' %}

{% block title %}
    <title>Éditer l'article</title>
{% endblock %}

{% block content %}

    <h1>Éditer l'article</h1>

    <form method="POST"></form>
        {% csrf_token %}
        {{ form }}
        <input type="submit" value="Éditer">
    </form>

{% endblock %}

Et voilà, un beau formulaire pour éditer notre article (ce n'est pas vrai : ce formulaire est toujours hideux) :

Créer la vue pour afficher un article

C'est bien beau tout ça, mais pour l'instant, on peut ajouter un article, le modifier, mais pas l'afficher.

Pour afficher un élément précis d'un modèle, nous allons utiliser la CBV DetailView !

On ajoute donc cette vue dans le fichier views.py :

# posts/views.py
from django.views.generic import DetailView

class BlogPostDetail(DetailView):
    model = BlogPost
    context_object_name = "post"

Cette vue s'attend par défaut à un template nommé blogpost_detail.html. Comme nous n'avons cessé d'être rebelle entre la partie précédente et cette partie, nous allons cette fois-ci donner à Django ce qu'il attend en créant ce fichier HTML à l'intérieur de notre dossier templates :

<!-- posts/templates/posts/blogpost_detail.html -->
{% extends 'base.html' %}

{% block title %}
    <title>{{ post.title }}</title>
{% endblock %}

{% block content %}
    <article></article>
        <h1>{{ post.title }}</h1>
        <div>{{ post.content|linebreaks|safe }}</div>
    </article>
{% endblock %}

On utilise la variable post que l'on a spécifié dans notre DetailView (dans le paramètre context_object_name). Par défaut vous pouvez également utiliser la variable object, mais c'est moins explicite.

Pour le contenu de l'article, on utilise le filtre linebreaks qui permet de convertir les retours à la ligne (\n) en retour à la ligne HTML (<br>).

Il ne nous reste plus qu'à relier un chemin d'URL à notre vue :

# posts/urls.py
from django.urls import path
from .views import BlogHome, BlogPostCreate, BlogPostEdit, BlogPostDetail

app_name = "posts"

urlpatterns = [
    path('', BlogHome.as_view(), name="home"),
    path('/', BlogPostDetail.as_view(), name="post"),  # On rajoute ce chemin
    path('create/', BlogPostCreate.as_view(), name="create"),
    path('edit//', BlogPostEdit.as_view(), name="edit"),
]

Et voilà, on peut maintenant afficher un article en lançant le serveur et en se rendant à l'adresse http://127.0.0.1:8080/blog/patrick-est-super/ (dans le cas d'un article vantant les mérites de Patrick) :

Créer la vue pour supprimer un article

Il ne nous reste plus qu'à ajouter la dernière vue des opérations CRUD : la vue Delete pour supprimer un article.

On crée donc une classe qui hérite de DeleteView :

# posts/views.py
from django.urls import reverse_lazy
from django.views.generic import DeleteView

class BlogPostDelete(DeleteView):
    model = BlogPost
    success_url = reverse_lazy("posts:home")

L'attribut success_url permet de spécifier l'adresse à laquelle nous souhaitons renvoyer l'utilisateur après la suppression d'un article. Dans ce cas-ci, nous redirigeons vers la vue d'accueil du blog.

Vous remarquez qu'on utilise la fonction reverse_lazy et non reverse. En effet, quand la classe est définie, la résolution des chemins d'URLs n'a pas encore été effectuée. Si vous utilisez reverse, vous aurez une belle erreur incompréhensible qui risque de vous retourner le cerveau de longues heures durant.

On crée ensuite un template HTML avec le nom attendu par Django (nomdumodele_confirm_delete.html), donc dans notre cas blogpost_confirm_delete.html :

<!-- posts/templates/posts/blogpost_confirm_delete.html -->
{% extends 'base.html' %}

{% block content %}

    <form method="POST"></form>
        {% csrf_token %}
        <p>Êtes-vous sûr de vouloir supprimer "{{ object }}"?</p>

        <input type="submit" value="Oui, supprimer">
    </form>

{% endblock %}

Sur cette page, on affiche tout simplement un formulaire qui nous demande si nous souhaitons réellement supprimer l'article.

Dans ce cas-ci, vous voyez qu'on utilise la variable object car nous n'avons pas modifié l'attribut context_object_name dans notre vue BlogPostDelete.

Et pour finir, vous connaissez la chanson, on relie un chemin d'URL à cette vue :

# posts/urls.py
from django.urls import path
from .views import BlogHome, BlogPostCreate, BlogPostEdit, BlogPostDetail, BlogPostDelete

app_name = "posts"

urlpatterns = [
    path('', BlogHome.as_view(), name="home"),
    path('/', BlogPostDetail.as_view(), name="post"),
    path('create/', BlogPostCreate.as_view(), name="create"),
    path('edit//', BlogPostEdit.as_view(), name="edit"),
    path('delete//', BlogPostDelete.as_view(), name="delete"),  # On ajoute la vue delete
]

Là encore, il faut inclure le slug de l'article dans le chemin d'URL pour que Django puisse savoir quel article supprimer.

Et voilà, on peut maintenant supprimer un article en nous rendant à l'URL dédiée, par exemple http://127.0.0.1:8080/blog/delete/patrick-est-super/ (Byebye Patrick) :

Ajouter de la colle

Notre application commence à avoir de la gueule ! On peut créer des articles, les modifier, les supprimer et les afficher.

On a également accès à tous les articles sur notre page d'accueil.

Mais il manque encore un peu de colle entre nos différents éléments. À chaque fois que nous souhaitons réaliser une opération, nous devons connaître l'URL et la taper dans la barre du navigateur.

Pour rendre la navigation et l'exécution des différentes actions CRUD plus pratique, nous allons donc ajouter quelques boutons dans nos différents fichiers HTML.

Nous allons user et abuser de la balise de gabarit url ainsi que des noms que nous avons donnés à nos différents chemins d'URLs. Cela nous permet de garder quelque chose de dynamique. Si vous souhaitez changer une adresse URL par la suite, tout continuera de fonctionner, car nous n'écrivons aucune URL en dur dans le code HTML.

Ajouter un menu de navigation

Pour commencer, nous allons ajouter un menu de navigation vraiment sommaire dans le fichier base.html.

Ce menu ne contiendra que deux liens :

  1. Un lien pour accéder à l'accueil du blog.
  2. Un lien visible uniquement par les utilisateurs connectés qui nous redirigera vers la vue pour ajouter un article.
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="fr"></html>
<head>
    <meta charset="UTF-8"></meta>
    {% block title %}{% endblock %}
</head>
<body>

<!-- On ajoute le menu à l'intérieur d'une balise nav -->
<nav></nav>
    <a href="{% url 'posts:home' %}">Accueil</a>
    {% if request.user.is_authenticated %}
        <a href="{% url 'posts:create' %}">Ajouter un article</a>
    {% endif %}
</nav>

<section id="blog">
    {% block content %}
    {% endblock %}
</section>

</body>
</html>

Plusieurs choses ici :

On utilise la balise url pour accéder à la page d'accueil. N'oubliez pas d'utiliser l'espace de nom de l'application (posts:home) : {% url 'posts:home' %}.

Faites attention aux guillemets que vous utilisez ! Ici, j'utilise des guillemets doubles pour entourer le lien de l'attribut href. Je dois donc utiliser des guillemets simples à l'intérieur de la balise url : "{% url 'posts:home' %}".On vérifie si l'utilisateur est connecté avec request.user.is_authenticated. Si c'est le cas, on affiche le deuxième lien qui nous permet d'ajouter un article (posts:create). On utilise ici la balise if du langage de gabarit de Django.

En ce moment, si vous essayez d'accéder à la page pour ajouter un article, vous devriez avoir une erreur :

Aucun objet Article trouvé en réponse à la requête.

Pourtant, on a bien utilisé l'url {% url 'posts:create' %} pourquoi Django nous indique-t-il que l'article n'a pas été trouvé 🤔

C'est une erreur très courante et qui perturbe beaucoup les débutants.

Pour la résoudre, il faut retourner voir nos chemins d'URLs :

# posts/urls.py

urlpatterns = [
    path('', BlogHome.as_view(), name="home"),
    path('/', BlogPostDetail.as_view(), name="post"),
    path('create/', BlogPostCreate.as_view(), name="create"),
    path('edit//', BlogPostEdit.as_view(), name="edit"),
    path('delete//', BlogPostDelete.as_view(), name="delete"),
]

L'ordre dans lequel nous ajoutons nos chemins a en effet beaucoup d'importance.

On ajoute en premier la vue de détail qui permet d'afficher un article.

Quand on se rend à l'adresse http://127.0.0.1:8080/blog/create/, Django pense donc que nous voulons afficher l'article avec le slug create.

Il faut donc inverser la 2e et la 3e url pour que quand nous nous rendions sur l'url blog/create/, Django nous renvoie bien vers la vue pour créer un article et non vers l'article create :

# posts/urls.py

urlpatterns = [
    path('', BlogHome.as_view(), name="home"),
    path('create/', BlogPostCreate.as_view(), name="create"),
    path('/', BlogPostDetail.as_view(), name="post"),
    path('edit//', BlogPostEdit.as_view(), name="edit"),
    path('delete//', BlogPostDelete.as_view(), name="delete"),
]

Ajouter des liens pour éditer et supprimer un article

On va maintenant ajouter deux liens qui vont rediriger vers la vue d'édition et de suppression d'un article.

Bien sûr, ces liens ne doivent être accessibles que pour un utilisateur connecté :

<!-- posts/templates/posts/blogpost_list.html -->
{% extends 'base.html' %}

{% block content %}

    <h1>Le blog de Docstring</h1>
    {% for post in posts %}
        <article>
            <h2>{{ post.title }}</h2>

            <div>
                {% if request.user.is_authenticated %}
                    <a href="{% url 'posts:edit' slug=post.slug %}">Éditer</a>
                    <a href="{% url 'posts:delete' slug=post.slug %}">Supprimer</a>
                {% endif %}
            </div>

            <h5>Publié par <i>{{ post.author_or_default }}</i> le {{ post.created_on|date:'j F Y' }}</h5>
            <p>{{ post.content|safe|truncatewords:50 }}</p>
        </article>
    {% endfor %}

{% endblock %}

Là encore, nous utilisons la balise if avec request.user.is_authenticated.

Pour les liens, n'oubliez pas d'envoyer le slug de l'article au paramètre slug. Il suffit pour ça de l'indiquer après le nom de l'URL : {% url 'posts:edit' slug=post.slug %}.

Ajouter un bouton pour lire l'article

La dernière chose qu'il nous manque, c'est un lien ou un bouton pour pouvoir accéder à la vue d'un article en particulier.

Nous allons donc modifier le fichier blogpost_list.html :

<!-- posts/templates/posts/blogpost_list.html -->
{% extends 'base.html' %}

{% block content %}

    <h1>Le blog de Docstring</h1>
    {% for post in posts %}
        <article>
            <h2>{{ post.title }}</h2>

            <div></div>
                {% if request.user.is_authenticated %}
                    <a href="{% url 'posts:edit' slug=post.slug %}">Éditer</a>
                    <a href="{% url 'posts:delete' slug=post.slug %}">Supprimer</a>
                {% endif %}
            </div>

            <h5>Publié par <i>{{ post.author_or_default }}</i> le {{ post.created_on|date:'j F Y' }}</h5>
            <p>{{ post.content|safe|truncatewords:50 }}</p>

            <!-- Un beau bouton -->
            <form action="{% url 'posts:post' slug=post.slug %}">
                <button>Lire l'article</button>
            </form>

        </article>
    {% endfor %}

{% endblock %}

Il existe plusieurs façons de créer un bouton qui redirige vers une page.

On pourrait utiliser une balise <a> et la styliser en CSS pour qu'elle ressemble à un bouton.

On pourrait ajouter un attribut onclick à notre bouton et un peu de JavaScript (avec location) pour rediriger vers une page lors du clique sur le bouton.

Dans notre cas, on passe par un tout petit formulaire qui ne contient que notre bouton. Pour rediriger vers la vue de l'article, on utilise l'attribut action auquel on passe la balise d'url qui redirige vers notre article.

Conclusion

Et voilà comment faire un petit blog moche !

Bien sûr, nous n'avons ici pas du tout abordé le coté front-end, nous avons donc un site digne des années 2000. Mais un site entièrement fonctionnel.

Vous remarquez qu'une fois qu'on a compris la base de Django, tout se ressemble. On crée des modèles, on crée des vues (CBVClass Based Views ou FBVFunction Based Views), on relie les chemins d'URLs aux vues et on pimente avec un peu de HTML et de langage de gabarit.

On pourrait ainsi continuer d'améliorer cette application de la même façon, en créant par exemple un modèle pour catégoriser nos articles. Nous pourrions également ajouter un modèle d'utilisateur personnalisé pour permettre à d'autres auteurs d'écrire des articles. Et pourquoi pas à des utilisateurs de commenter les articles (grâce à un modèle de commentaires).

Bref, une fois que vous avez compris les bases, sky's the limit, comme on dit en Bretagne.