Wagtail Photography 6

Remember how in the previous post and in the previous previous post and probably even before that I mentioned the sub-project pages? Well, today we will implement them. Now that we have the various blocks that we need, let’s build our base, find the end portal, defeat the dragon..oops wrong blog

So let’s add to our projects/models.py file. First we import the blocks we worked on and a few other things we will use:

from django.db import models
from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField, StreamField
from wagtail.admin.edit_handlers import FieldPanel
from modelcluster.fields import ParentalKey
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel, InlinePanel, MultiFieldPanel
from wagtail.core import blocks
from wagtail.images.edit_handlers import ImageChooserPanel
from streams import blocks
from django.db.models import TextField

Now the code for the SubProjectPage. But first I realised I didn’t explicitly say what template was used for the ProjectsPage, so let’s just add that quickly. But then I also remembered why I didn’t add it but oh well.

class ProjectsPage(Page):
    """
    Projects page with only an intro.
    """
    template = "projects/projects_page.html"

    intro = RichTextField(blank=True)
    content_panels = Page.content_panels + [
        FieldPanel('intro', classname="full")
    ]
class SubProjectPage(Page):
    """Subproject page with flexible layout."""
    template = "projects/subproject_page.html"

    intro = models.TextField(blank=True)
    author = models.CharField(max_length=255)
    date = models.DateField("Post date")
    main_image = models.ForeignKey(
        "wagtailimages.Image", on_delete=models.SET_NULL,
        related_name="+",
        null=True,
        blank=False,
    )
    stream_body = StreamField(
        [
            ("title_and_text", blocks.TitleAndTextBlock()),
            ("full_richtext", blocks.RichTextBlock()),
            ("cards", blocks.CardBlock()),
            ("empty", blocks.EmptyBlock()),
        ],
        null=True,
        blank=True
    )
    content_panels = Page.content_panels + [
        FieldPanel('intro'),
        FieldPanel('author'),
        FieldPanel('date'),
        ImageChooserPanel('main_image'),
        StreamFieldPanel('stream_body'),

    ]

Let’s understand what is going on. We specify the template (will look at that shortly), add a few fields including intro, author, date and main_image (remember what the main_image is used for?). Then onto the important stuff: we have stream_body and we have content_panels. The stream_body refers to our StreamField and its main paraeter is a list of (name, block_type) tuples. The name is only used to identify the block type within templates and the way the StreamField is saved in its JSON representation. The block_type refers to the block definition objects that we imported. So in our StreamField we use our TitleAndTextBlock, RichTextBlock, CardBlock and EmptyBlock. This doesn’t actually mean they all have to be used within the page. The person creating the page can choose to use those blocks as many times as they wish, in any order. Notice I also set the blank parameter equal to True, meaning that it can be blank. If set to False, at least one block must be provided inside the StreamField. (is there any reason someone would create a StreamField with no blocks in it? Not sure about that, will look into it, but probably not).

Now let’s add everything to our content_panels (remember they’re what visible when a Page is edited). We add the intro, author and date as FieldPanels, main_image as an ImageChooserPanel and finally stream_body as a StreamFieldPanel. And that’s our SubProjects page. You want to see the template? sure. First of all please note I am using Justified Gallery and fslightbox.js for the gallery. Why two things? One is for creating a justified gallery that adjust size when the page is resized and one is for the lightbox.

{% extends "base.html" %}
{% load static wagtailcore_tags wagtailimages_tags %}

{% block extra_css %}
    <link href="{% static 'css/justifiedGallery.min.css' %}" rel="stylesheet">
{% endblock %}

{% block extra_js %}
    <script src="{% static 'js/jquery.justifiedGallery.min.js' %}"></script>
    <script src="{% static 'js/fslightbox.js' %}"></script>

    {% for block in page.stream_field %}
        {% if block.block_type == 'cards' %}
            <script>
                $('#gallery{{forloop.counter}}').justifiedGallery({
                    rowHeight : 400,
                    lastRow : 'center',
                    margins : 3
                });
            </script>
        {% endif %}
    {% endfor %}

{% endblock %}

{% block content %}

<div class="container-fluid mb-3 mt-6 mb-sm-3 mt-sm-3">
    {% for block in page.stream_field %}
        {% if block.block_type == 'cards' %}
            <div id="gallery{{forloop.counter}}" class="justified-gallery">
                {% include_block block %} 
            </div>
        {% else %}
            {% include_block block %}
        {% endif %}
    {% endfor %}
</div>

{% endblock %}

We load a few tags from wagtail that we need to use. We include the extra css and js files we need for our galleries. Don’t forget to add them to your project’s static css and js folders. Now there is some extra logic we perform in the js section. We iterate over the blocks in page.stream_field and if the block type is ‘cards’ (remember the cards refer to our images?), then we add them to our gallery. Why is it that we set the #gallery{{forloop.counter}}? Because we will have multiple galleries on the page.

Then in our main block for content, we iterate over the blocks in the page’s stream_field. If it’s of type ‘cards’ (i.e. related to the image gallery), we add it to a gallery div. Otherwise, we just include the block. And that’s it basically for our SubProjects page.

What about Simple Subproject page? What was that? That was a page with maybe some text at the beginning and then just one big gallery. Let’s look at that as well. The code for it is (again, to be added at projects/models.py):

class SimpleSubProjectPage(Page):
    """Simple experience page with intro and gallery."""
    template = "projects/simple_subproject_page.html"

    author = models.CharField(max_length=255)
    date = models.DateField("Post date")
    intro = TextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('author'),
        FieldPanel('date'),
        FieldPanel("intro"),
        MultiFieldPanel([
            InlinePanel("gallery_images", max_num=200,
                        min_num=1, label="Image"),
        ], heading="Gallery Images"),

    ]


class SimpleSubProjectPageImage(Orderable):
    page = ParentalKey(SimpleSubProjectPage, on_delete=models.CASCADE,
                       related_name='gallery_images')
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=False,
        on_delete=models.SET_NULL,
        related_name='+'
    )
    caption = models.CharField(blank=True, max_length=250)

    panels = [
        ImageChooserPanel('image'),
        FieldPanel('caption'),
    ]

What does the SimpleSubProjectPage have? It has the template defined (just good practice for anyone who then jumps into the code). an author, a date and an intro. Note that the intro is of TextField that comes from Django. It is not Wagtail’s RichTextField in order to keep all these pages consistent and apply the styling in the template. Then we add the author, date and intro as FieldPanels in our content_panels and then a MultiFieldPanel with an InlinePanel only (for now) that refers to gallery_images..hmmm? What’s that? Well, that refers to the Orderable that we defined below as SimpleSubProjectPageImage (don’t worry, I will choose an even longer name next time.. I’m not good at naming stuff). It has a ParentalKey that is the SimpleSubProjectPage and a related_name of gallery_images. There it is: that’s why we refer to gallery_images in our SimpleSubProjectPage. The Orderable has an image and a caption. For its panels (again, it’s panels and not content_panels because we are not “editing” a page). So in our SImpleSubProjectPage, we can have an orderable where we add images. That’s all. Now for the template:

{% extends "base.html" %}
{% load static wagtailcore_tags wagtailimages_tags %}

{% block extra_css %}
<link href="{% static 'css/justifiedGallery.min.css' %}" rel="stylesheet">
{% endblock %}

{% block extra_js %}
<script src="{% static 'js/jquery.justifiedGallery.min.js' %}"></script>
<script src="{% static 'js/fslightbox.js' %}"></script>

<script>
    $('#gallery').justifiedGallery({
        rowHeight: 400,
        lastRow: 'center',
        margins: 3
    });
</script>

{% endblock %}

{% block content %}

<div class="container-fluid mb-5 mb-sm-5 mt-sm-5">
    <div class="row display-flex no-gutters">
        <div class="col">
            <p class="text-center text-wrap px-n3">{{self.intro}}</p>
        </div>
    </div>

    <div id="gallery" class="justified-gallery">

        {% for loop_cycle in self.gallery_images.all %}
            {% image loop_cycle.image height-2000 as orig_img %}
            {% image loop_cycle.image height-800 as img %}
            <a data-fslightbox="gallery1" href="{{orig_img.url}}">
                <img src="{{img.url}}">
            </a>
        {% endfor %}

    </div>
</div>

{% endblock %}

It’s similar to the SubProjects page but in the js section, we don’t number the galleries. Similarly, we don’t number anything in the main gallery and we don’t check for blocks as they are not StreamField blocks anymore. Rather we loop through the self.gallery_images which we defined in the SimpleSubProjectPage earlier. And there you have it! Now we have Projects, SubProject, SimpleSubProject pages. In the next post we will look at adding a Menu. I think it’s time no?