pythonwagtailwagtail-snippet

How to create a block/template/snippet in Wagtail?


Need directions, documentation wasn't clear enough so I'm bringing up this question. I want to make a custom carousel using as a base the coderedcms implementation that resembles slick-slider (reference website: www.fcs.mg.gov.br). I think it's an absolute simple thing to do but I'm just now learning Wagtail. Basically creating a custom block, adding a template to it and registering it as snippet so it's reusable. An example of how a tree would look like with those files would be great too. Thanks.


Solution

  • I hope you'll be using lighter images than the example 😉 ... some of those slide images were 5+MB ... 40MB page size! This doc will guide on using image filters in your templates to reduce dimensions, set quality (ie compression) and choose a lighter image type (convert image to webp etc). Images that display size should be no more than about 30-40kB.

    There are multiple ways of tackling this use case.

    Do you want the same collection of images to be used on multiple pages and administered centrally from one point so that any changes are reflected site-wide?

    If yes - registering a snippet model would be the way to go. I would use a ClusterableModel/Orderable setup for this so that you can control the order in which the images display. This doc is an example of that, where each image would be an orderable instance (BandMember in the example, and each image collection is a Band).

    Where you save this to really depends on your project structure and what apps you already have. You could save it to a related app or to an app just dedicated for the gallery.

    *** disclaimer - I've hacked the code below from existing code I have in projects, so everything is untested - if I've made an error, let me know :) ***

    If you had a dedicated gallery app, your snippet model might be:

    # /gallery/slidergallery.py
    
    from django.utils.translation import gettext_lazy as _
    from django.db import models
    from modelcluster.fields import ParentalKey
    from modelcluster.models import ClusterableModel
    from wagtail.models import Orderable
    from wagtail.admin.panels import FieldPanel, InlinePanel
    from wagtail.snippets.models import register_snippet
    from wagtail.models import PreviewableMixin
    
    class GalleryImage(Orderable):
        gallery = ParentalKey("gallery.SliderGallery",
                              related_name="gallery_images", on_delete=models.CASCADE)
        image = models.ForeignKey(
            'wagtailimages.Image',
            blank=True,
            null=True,
            related_name='+',
            on_delete=models.SET_NULL,
            verbose_name=_("Gallery Image"),
            help_text=_(
                "<< some information about image aspect ratio used, minimum size etc >>")
        )
        title = models.CharField(label=_("Optional Image Title"), required=False)
        caption = models.TextField(
            label=_("Optional Image Caption"), required=False)
        link = models.ForeignKey(
            "wagtailcore.Page",
            null=True,
            blank=True,
            related_name="+",
            on_delete=models.SET_NULL,
            verbose_name=_("Optional Link to Internal Page"),
        )
    
    
    @register_snippet()
    class SliderGallery(PreviewableMixin, ClusterableModel):
        heading = models.CharField(
            label=_("Carousel Title"),
            required=False,
        )
    
        panels = [
            FieldPanel("heading"),
            InlinePanel("gallery_images", min_num=2, max_num=5)
        ]
    
        def get_preview_template(self, request, mode_name):
            return "previews/slider_gallery.html"
    

    I've added the PreviewableMixin so that you can see the slider for your gallery instance in the live preview without needing to add it to a page first. The slider_gallery.html would just be a template that can render the slider as a standalone object.

    I'd recommend also using DraftStateMixin and LockableMixin for a use case like this.

    To link a gallery instance to your page model, you would create a standard Django ForeignKey field, something like:

    from gallery.slidergallery import SliderGallery
    
    class YourPageWithTheGallery(Page) 
        ...   
        slider = models.ForeignKey(
            "gallery.YourGalleryModel",
            null=True,
            blank=False,
            related_name="+",
            on_delete=models.SET_NULL,
            verbose_name=_("Slider Gallery"),
        )
        ...
    

    In your page's panels section, you just need a standard FieldPanel for this field.

    If you want the code reusable but each page should have a unique image collection then you have two more choices:

    1. There is only one gallery per page, always displayed in the same place
    2. There can be none or multiple galleries per page, the position can change.

    Option 1 - you just need the orderable class discussed above without any need for the ClusterableModel. Instead, you just need the Orderable with a ParentalKey that links back to your Page type. If each item in the gallery was an image with some other fields, it'll be something like:

    from django.db import models
    from modelcluster.fields import ParentalKey
    class GalleryImage(Orderable):
        page = ParentalKey(
            "your_page_app.YourPageClass", 
            related_name="gallery_images", 
            on_delete=models.CASCADE)
        image = models.ForeignKey(
            'wagtailimages.Image',
            blank=True,
            null=True,
            related_name='+',
            on_delete=models.SET_NULL,
            verbose_name=_("Gallery Image"),
            help_text=_(
                "<< some information about image aspect ratio used, minimum size etc >>")
        )
        title = models.CharField(label=_("Optional Image Title"), required=False)
        caption = models.TextField(
            label=_("Optional Image Caption"), required=False)
        link = models.ForeignKey(
            "wagtailcore.Page",
            null=True,
            blank=True,
            related_name="+",
            on_delete=models.SET_NULL,
            verbose_name=_("Optional Link to Internal Page"),
        )
    

    For this, I would generally add this to the same models.py that the related Page model is defined (it should be defined before the page), otherwise, if you define it in a separate module, make sure it's imported so that it is loaded in time for the InlinePanel in the page.

    Note the related_name attribute in the ParentalKey. On your page model, you don't need to add a field for the gallery, instead, you just need to add an InlinePanel to your panels section and use this related_name:

        MultiFieldPanel(
            [InlinePanel("carousel_images", max_num=5, min_num=2)],
            heading="Carousel Images",
        ),
    

    You can set minimum and maximum images as above, or leave one or both of these out.

    Once you've done that, you'll see the "Carousel Images" field in your page editor where you can add images and other attributes. In your template, you can access the gallery via self.carousel_images, e.g.

    {% for img in self.carousel_images %}
        ....
    {% endfor %}
    

    Option 2, where you can have none or multiple galleries with varying locations on the page, you'd create a pair of custom StructBlocks (one each for gallery item and gallery) and add the gallery block to your page's StreamField. E.g.

    from django.utils.translation import gettext_lazy as _
    from wagtail.blocks import (CharBlock, ListBlock,
                                PageChooserBlock, StructBlock, TextBlock)
    from wagtail.images.blocks import ImageChooserBlock
    
    class CarouselImageBlock(StructBlock):
        image = ImageChooserBlock(label=_("Select Image & Enter Details"))
        title = CharBlock(label=_("Optional Image Title"), required=False)
        caption = TextBlock(label=_("Optional Image Caption"), required=False)
        link = PageChooserBlock(
            required=False,
            label=_("Optional Link to Internal Page")
        )
    
        class Meta:
            icon = 'image'
            label = _("Image for Carousel")
    
    
    class ImageCarouselBlock(StructBlock):
        heading = CharBlock(
            label=_("Carousel Title"),
            required=False,
        )
        carousel_images = ListBlock(CarouselImageBlock, min_num=2, max_num=5)
    
        class Meta:
            template = 'blocks/image_carousel.html'
            icon = "image-carousel"
            label = _("Image Carousel")
            label_format = label
    

    template = 'blocks/image_carousel.html' will point to the block rendering template where you can iterate through the items in the same as above, e.g.:

    {% for slide in self.carousel_images %}
        ....
    {% endfor %}
    

    Because blocks can be reused across multiple page types and projects, I prefer to keep a separate blocks app and define each in its own module. The models.py for that app is just importing all the blocks I use throughout the project - it simplifies the imports on the pages there being used on.

    There are other options, but one of these would probably meet your needs.