Ajouter StreamField à une page Wagtail

Dans cet article, on va ajouter un StreamField à une page Wagtail, nous permettant de saisir du texte et des images en ligne via l'éditeur CMS.

8 Juillet 2020 12:25
Thèmes: Options de Wagtail Streamfield

Supposons que vous ayez installé Wagtail et créé une page d'accueil très basique. En utilisant le StreamField de Wagtail, on peut ajouter du texte et des images aux pages de manière très flexible. On va créer un modèle de page d'article qui donnera un aperçu de tous les articles. Les articles seront dans l'ordre chronologique inverse. Étant donné que la documentation de Wagtail contient un très bon tutoriel, on va plonger directement un peu plus profondément.

Dans models.py, ajoutez un modèle ArticlePage (si vous avez déjà des importations, supprimez les doublons):

from django.db import models
from django.utils.translation import gettext_lazy as _
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, StreamFieldPanel
from wagtail.core import blocks
from wagtail.core.fields import RichTextField, StreamField
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtailtrans.models import TranslatablePage
from .blocks import InlineImageBlock


class ArticlePage(TranslatablePage):
    intro = RichTextField(blank=True)
    image = models.ForeignKey(
        'wagtailimages.Image', blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("Image")
    )
    featured = models.BooleanField(default=False)
    body = StreamField([
        ('paragraph', blocks.RichTextBlock()),
        ('image', InlineImageBlock()),
   ])

    content_panels = TranslatablePage.content_panels + [
        FieldPanel('intro'),
        FieldPanel('featured'),
        ImageChooserPanel('image'),
        StreamFieldPanel('body'),
   ]

Le champ d'image n'est pas un champ obligatoire, donc blank=True, null=True et on_delete = models.SET_NULL. On définisse related_name égal à '+', car on ne veut pas créer une relation en arrière de l'image à la page. Le champ featured sera utilisé pour sélectionner des articles pour notre page d'accueil. StreamField permet de mélanger du texte, des images, des vidéos, des citations, des extraits de code, etc. dans une seule entité. Il se compose de (nom, block_type) tuples. Wagtail fournit beaucoup de blocs prêts à l'emploi. Le RichTextBlock est Wagtail standard.

InlineImageBlock est un bloc personnalisé qu'on va créer à l'aide de StructBlock. Bien que cela ne soit pas strictement nécessaire, il est judicieux de placer vos modèles StructBlock dans un fichier séparé; le site de démonstration de référence de Wagtail le fait comme ça. Créez donc un blocks.py dans votre application et ajoutez le contenu suivant:

from django.utils.translation import gettext_lazy as _
from wagtail.core import blocks
from wagtail.core.blocks import CharBlock
from wagtail.images.blocks import ImageChooserBlock


class InlineImageBlock(blocks.StructBlock):
    image = ImageChooserBlock(label=_("Image"))
    caption = CharBlock(required=False, label=_("Caption"))
    float = blocks.ChoiceBlock(
        required=False,
        choices=[('right', _("Right")), ('left', _("Left")), ('center', _("Center"))],
        default='right',
        label=_("Float"),
    )
    size = blocks.ChoiceBlock(
        required=False,
        choices=[('small', _("Small")), ('medium', _("Medium")), ('large', _("Large"))],
        default='small',
        label=_("Size"),
    )

    class Meta:
        icon = 'image'

Toutes les phrases lisibles par l'homme sont traduites à l'aide de gettext_lazy. image et caption parlent d'elles-mêmes. Le champ float sera utilisé dans le modèle pour positionner l'image à droite, à gauche ou au centre de la page; le paramètre size fonctionne de la même manière. Les deux sont ChoiceBlocks. La classe Meta permet de définir l'argument icon. Vous pouvez afficher une liste de toutes les icônes disponibles dans le guide de style Wagtail; si vous ne souhaitez pas l'installer, recherchez les icônes Wagtail StreamField sur le Web. Pour notre objectif, image fera l'affaire.

Le modèle est prêt, il est temps de créer le modèle. Comme on l'a vu précédemment avec la page d'accueil, le nom du modèle doit être article_page.html. Vous pouvez afficher le contenu sur Github, ou bien regarder la vidéo dans cet article. Les éléments habituels tels que le chargement des bibliothèques, le titre de la page, l'intro, la date se font comme dans home_page.html. Les seuls nouveaux éléments sont les suivants:

{%  include "streamfield.html" %}
<a href="{{ page.get_parent.url }}">{% trans "Return to articles" %}</a>

c'est-à-dire un modèle streamfield.html et un lien vers la page parent (qu'on va créer ci-dessous). Les deux sont emballés dans un conteneur pour les rendre plus beaux sur la page. Le fichier streamfield.html sera utilisé chaque fois qu'un bloc doit être rendu et a le contenu suivant:

{% load wagtailcore_tags wagtailimages_tags %}

{% for block in page.body %}
    {% if block.block_type == 'image' %}
        <div class="block-{{ block.block_type }}-{{ block.value.float }}">
            <!-- make use of specific properties of Wagtail 'image' tag -->
            {% if block.value.size == "small" %}
                {% image block.value.image width-240 class="img-fluid" %}
            {% elif block.value.size == "medium" %}
                {% image block.value.image width-480 class="img-fluid" %}
            {% else %}
                {% image block.value.image width-2400 class="img-fluid" %}
            {% endif %}
            {{ block.value.caption }}
        </div>
   {% else %}
        <div class="block-{{ block.block_type }}">
            {% include_block block %}
        </div>
    {% endif %}
{% endfor %}

Une distinction est faite entre les différents types de blocs: image et autres. On référence les champs individuels des StructBlocks via la propriété value; idem pour la propriété block_type. Pour les images, on utilise la balise de modèle image et les options de rendu d'image de Wagtail. La classe img-fluid de Bootstrap permet à toutes les images de s'intégrer parfaitement dans la fenêtre. La balise include_block prend en charge le rendu de tous les autres blocs (non image).

Il est temps d'ajouter un peu de style à notre CSS (pour la première fois dans notre projet, pour ceux d'entre vous qui l'ont suivi). Les éléments de style essentiels pour les modèles ci-dessus sont (j'en ai laissé certains qui sont simples, par exemple block-paragraph, block-image-right/left, block-video-right/left, block-small/medium/large, ils sont en le référentiel):

.img-fluid {
    max-width: 100%;
    height: auto;
}
/* for the images and text inside streamfields */
.block-image-center {
    display: grid;
    /* justify-content works here because Wagtail creates an image of fixed dimensions */
    justify-content: center;
    overflow: hidden;
    font: italic 12px Georgia, serif;
}

Le centrage d'une image fonctionne mieux avec justify-content car Wagtail crée une image de dimensions fixes qui est ensuite centrée dans le div avec la propriété justify-content.

Notre page d'article est prête maintenant. On crée maintenant une page d'index contenant tous les articles qu'on a l'intention d'écrire, dans nos models.py:

class ArticleIndexPage(TranslatablePage):
    intro = RichTextField(blank=True)

    # Specifies that only ArticlePage objects can live under this index page
    subpage_types = ['ArticlePage']

    # A method to access and reorder the children of the page (i.e. ArticlePage objects)
    def articlepages(self):
        return ArticlePage.objects.child_of(self).live().order_by('-first_published_at')

    def featured_articlepages(self):
        return self.articlepages().filter(featured=True)

    content_panels = TranslatablePage.content_panels + [
        FieldPanel('intro', classname='full'),
    ]

Le paramètre subpage_types garantit qu'on crée uniquement des instances ArticlePage sous cette page d'index. La méthode articlepages sélectionne les sous-pages actives de cette page d'index et les réorganise de manière anti-chronologique. Le champ first_published_at est un champ standard du modèle de page. La méthode featured_articlepages est un sous-ensemble: seuls ceux qui sont marqués featured pour la page d'accueil. On utilise la méthode articlepages dans le modèle article_index_page.html; la partie intéressante du modèle est:

{% for childpage in page.articlepages %}
    <div class="col-auto mb-3">
        <div class="card article">
            <a href="{{ childpage.url }}">
                {% if childpage.specific.image %}
                    {% image childpage.specific.image fill-320x240 class="img-front rounded" %}
                {% else %}
                    <img alt="" src="{% static 'images/transparent.png' %}" width="320" height="240" class="img-default rounded">
                {% endif %}
                <img alt="" src="{% static 'images/transparent.png' %}" width="320" height="240" class="img-background rounded">
                <div class="card-img-overlay">
                    <h5 class="card-title">{{ childpage.title }}</h5>
                    <p class="card-subtitle">{{ childpage.specific.intro|striptags|safe|truncatewords:15 }}</p>
                    <p class="card-text small">{{ childpage.specific.first_published_at }}</p>
                </div>
            </a>
        </div>
    </div>
{% endfor %}

Pour chaque sous-page, un lien est créé sous la forme d'une image de 320 x 240 pixels. On utilise les classes Bootstrap pour les cartes, telles que card-img-overlay. Sur l'image se trouve le titre de la page plus les 15 premiers mots de l'intro et la date. On utilise le filtre striptags de Django pour supprimer toutes les balises html dans le titre, le filtre safe pour empêcher d'échapper les citations dans le texte et le filtre truncatewords pour tronquer l'intro après 15 mots. Comme expliqué dans la documentation de Wagtail, on doit utiliser la méthode specific pour référencer les champs des enfants de l'instance ArticlePage.

Si la page enfant contient une image, cette image est utilisée, sinon, l'image images/transparent.png est utilisée. Il s'agit d'une image entièrement transparente (comparez-la à une feuille de verre) facile à créer ou à télécharger; placez-le dans le sous-répertoire images de cms/static/cms et ajoutez ce répertoire à STATICFILES_DIRS dans les paramètres. En utilisant la propriété background-color, on peut la transformer en n'importe quelle couleur; la classe image-default dans nos paramètres CSS la rendra grise. La deuxième image avec la classe img-background est une superposition qui change de couleur lorsque vous la survolez avec la souris. La superposition d'image est obtenue par la combinaison des déclarations position: relative et position: absolute. On utilise la classe Bootstrap rounded pour arrondir le coin des images. Les paramètres CSS sont les suivants:

.article, .theme {
    width: 320px;
    height: 240px;
}
.img-front {
    position: relative;
    max-width: 100%;
}
.img-default {
    position: relative;
    max-width: 100%;
    background-color: grey;
}
.img-background {
    position: absolute;
    max-width: 100%;
    max-height: 100%;
    left: 0;
    background-color: navy;
    opacity: 0;
    transition: opacity 300ms;
}
a:hover .img-background {
    opacity: 0.6;
}

Maintenant, on veut avoir un lien vers la page d'index des articles ainsi que les articles en vedette sur notre page d'accueil. Au modèle HomePage, ajoutez:

article_section_title = models.CharField(
    null=True,
    blank=True,
    max_length=255,
    help_text=_("Title to display above the article section"),
)
article_section_intro = RichTextField(blank=True)
article_section = models.ForeignKey(
    TranslatablePage,
    null=True,
    blank=True,
    on_delete=models.SET_NULL,
    related_name='+',
    help_text=_("Featured articles for the homepage"),
    verbose_name=_("Article section"),
)

Ajoutez les importations MultiFieldPanel et PageChooserPanel et aux content_panels du modèle HomePage, ajoutez:

MultiFieldPanel([
        FieldPanel('article_section_title'),
        FieldPanel('article_section_intro', classname='full'),
        PageChooserPanel('article_section'),
        ], heading=_("Article section"), classname='collapsible'),

Cela nous permet de créer une section d'article avec un titre, une introduction et un lien vers TranslatablePage; en admin, on va lui assigner la page d'index de l'article, afin qu'on y ait accès dans notre modèle. Là, on ajoute un morceau de html qui ressemble beaucoup à ce qu'on vient de créer pour notre article_page.html, seule la boucle sur tous les articles est différente:

{% for childpage in page.article_section.specific.featured_articlepages %}
{% endfor %}

On doit faire entrer wagtailimages_tags et static car on utilise la balise image de Wagtail et l'image transparent.png dans notre répertoire statique.

Ouf, c'est fini! Migrez la base de données pour intégrer toutes les modifications de modèle. Ensuite, accédez à l'admin de Wagtail, aux pages et à notre page d'accueil et créez une page enfant avec le modèle ArticleIndexPage. Donnez-lui un titre et tapez du texte dans le champ d'introduction; publiez-le. Créez ensuite une page enfant de cette page d'index (elle utilisera automatiquement le modèle ArticlePage), ajoutez du texte, téléchargez et insérez des images et publiez. Créez une autre page d'article. Affichez les liens de l'image sur la page d'accueil et la page d'index. De toute évidence, n'hésitez pas à changer le style.

Avec cela, la base pour mettre du contenu sur notre site est terminée. Vous pouvez en savoir plus sur l'intégration de la vidéo, ajouter des thèmes et navigation.

Commentez cet article (connectez-vous d'abord ou confirmez par nom et email ci-dessous)