[PYTHON] Wagtail Recommendations (3) Understand and use the tree structure of pages

Introduction

On the Wagtail management site, a new page instance is created by ADD CHILD PAGE on an existing page. For example, remember that last time we created one instance of the TopPage class as a child element of the HomePage class instance ("Welcome to your new Wagtail site!" Page) that is provided by default.

As a result, every page instance (other than the default HomePage instance) has only one parent element. Also, each page instance can have multiple child elements. In this way, it can be said that all the page instances in Wagtail are connected to each other by the parent-child relationship defined by the tree structure.

The URL of each page is also automatically assigned according to this tree structure (so it is not necessary to specify the routing in urls.py). For example, the default HomePage instance address is the default

https: // host name /

If it remains, the address of a TopPage instance created as its child element is

https: // hostname/hello /

(Hello is the character string specified in Slug on this page). Furthermore, if the Slug string of another page generated as its child element is world, the address of that page is

https: // hostname/hello/world /

That is to say.

This time, after adding the PlainPage class, increase the number of page instances on the management site, experience this tree structure, and try a little technique using it.

Add page class

First, in addition to the previous TopPage, add the definition of PlainPage. Specifically, cms/models.py was extended as shown below.

from django.db import models

from modelcluster.fields import ParentalKey

from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, PageChooserPanel
from wagtail.images.edit_handlers import ImageChooserPanel

class TopPage(Page):
    ...

class PlainPage(Page):
    cover_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )
    intro = models.CharField(max_length=255)
    main_body = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        ImageChooserPanel('cover_image'),
        FieldPanel('intro'),
        FieldPanel('main_body', classname="full"),
    ]

    parent_page_types = ['cms.TopPage']
    subpage_types = []

    def get_top_page(self):
        pages = self.get_ancestors().type(TopPage)
        return pages[0]

Since cover_image, intro, and main_body are also fields in TopPage, there is no need to explain up to content_panels. parent_page_types and subpage_types are descriptions to limit the page classes that can be associated with this image class in a parent-child relationship. Specifically, it specifies that the parent element of PlainPage must be TopPage and that it cannot have child elements.

get_top_page () is a self-made method that returns the TopPage instance that is its parent element. In this self-made method, I use a method called get_ancestors (), which is one of the methods to get other pages by using the parent-child relationship between page instances provided by Wagtail. Is. There are other similar methods available, so check here.

Now that you have defined the PlainPage class, log in to your admin site and create somePlainPage instances as child elements of the previously created TopPage instance (you'll use them later). ..

Holding navigation items

Now that we've defined a new page class and created some instances of it, we'll want to display them in the browser next, but before that, let's add a little extension to the TopPage class as well. This is a preparation for implementing the nav_bar block and footer block, which were left as dummies last time. Specifically, the code was extended as follows.

class TopPage(Page):
    ...
    footer_text = models.CharField(blank=True, max_length=255)

    content_panels = Page.content_panels + [
        ...,
        FieldPanel('footer_text'),
        InlinePanel('nav_items', label="Nav items"),
    ]

    def get_top_page(self):
        return self

class PlainPage(Page):
    ...

class NavItems(Orderable):
    top_page = ParentalKey(TopPage, related_name='nav_items')
    label = models.CharField(max_length=255)
    page = models.ForeignKey(
        Page,
        on_delete=models.CASCADE,
        related_name='+'
    )
    panels = [
        FieldPanel('label'),
        PageChooserPanel('page'),
    ]

The footer_text added to the TopPage class is for storing the text to be displayed in the footer. Looking at content_panels, in addition to this panel for footer_text, InlinePanel for nav_items is added. What is this?

To understand this, we need to refer to the definition of the NavItems class below. First, we can see that the NavItems class is defined as a subclass that inherits the Orderable class. The Orderable class is a class provided by Wagtail so that the parts of a page can be defined independently of the page and can be linked to the page in a many-to-one manner. Here, this is used to manage the items (link destination page page and its label label) installed in the navigation bar.

The landing page page of each navigation item can be linked to the Page class with models.ForeignKey. And use PageChooserPanel to edit it. Note that in the Orderable class, the edit panel is specified by panels instead of content_panels.

This NavItems class is associated with the TopPage class as a part, and for this, the ParentalKey of the django-model cluster is used as described in the top_page field. Then, on the edit screen of the TopPage class, the instance of the NavItems class can be edited using the InlinePanel added to the contant_panels.

Also note that the get_top_page () method has been added to the TopPage class as well. This is a method that returns itself in the case of the TopPage class. The reason for adding such a method is clarified below (so please be patient and read on).

At this stage, log in to the management site again, open the edit screen of the TopPage instance created last time, and add some instances of the NavItems class along with the character string of footer_text.

Creating a navigation bar and footer

Now that you are ready, proceed to view the page. First, let's display the newly created Plain Page. The template templates/cms/plain_page.html for this class is defined as below.

{% extends 'cms/base.html' %}
{% load static wagtailcore_tags wagtailimages_tags %}

{% block nav_bar %}{% endblock %}

{% block header %}
    {% if page.cover_image %}
        {% image page.cover_image fill-1000x400 as my_image %}
    {% else %}
        {% image page.get_top_page.specific.cover_image fill-1000x400 as my_image %}
    {% endif %}
    <div class="jumbotron jumbotron-fluid" style="background-image: url('{{ my_image.url }}');">
        <div class="container">
            <h1 class="display-4">{{ page.title }}</h1>
            <p class="lead">{{ page.intro }}</p>
        </div>
    </div>
{% endblock %}

{% block main %}
    <div class="container">
        <div class="row">
            <div class="col-md-8">
                {% block main_body %}
                    <div class="rich-text my-5">
                        {{ page.main_body|richtext }}
                    </div>
                {% endblock %}
            </div>
            <div class="col-md-4">
                {% block side_bar %}
                    <div class="card my-4">
                        <h4 class="card-header">
                            {{ page.get_top_page.specific.side_title}}
                        </h4>
                        <div class="card-body">
                            {% image page.get_top_page.specific.side_image fill-200x200 class="img-fluid rounded-circle d-block mx-auto" alt="" %}
                        </div>
                        <div class="card-body">
                            <div class="rich-text">
                                {{ page.get_top_page.specific.side_body|richtext }}
                            </div>
                        </div>
                    </div>
                {% endblock %}
            </div>
        </div>
    </div>
{% endblock %}

{% block footer %}
    <nav class="navbar navbar-dark bg-dark fixed-bottom">
        <span class="navbar-text mx-auto py-0">
                {{ page.get_top_page.specific.footer_text }}
        </span>
    </nav>
{% endblock %}

{% block extra_js %}
    {{ block.super }}
    <script type="text/javascript" src="{% static 'js/navbar.min.js' %}"></script>
    <script>
        const nav_props = {
            color: "dark",
            variant: "dark",
            brandlabel: "{{ page.get_top_page.specific.title }}",
            brandhref: "{% pageurl page.get_top_page %}",
            navs: [
                {% for nav_item in page.get_top_page.specific.nav_items.all %}
                    {
                        label: "{{ nav_item.label }}", 
                        href: "{% pageurl nav_item.page %}"
                    },
                {% endfor %}
            ],
        };

        ReactDOM.render(
            React.createElement(MyNavbar, nav_props),
            document.getElementById('nav_bar')
            );
    </script>
{% endblock %}

Let's start with the header block. It is basically the same as the description in the previous templates/cms/top_page.html, but as the background image my_image, if cover_image is not registered in this PlainPage instance itself,get_top_page () It can be seen that it is devised to use thecover_image of the TopPageinstance of the parent element acquired by themethod.

Also, at that time, you may have noticed that it is page.get_top_page.specific.cover_image instead of page.get_top_page.cover_image. Looking at the entire code, there are some other places where .specific is added. What is this .specific?

In fact, if you get another page instance with the get_top_page () method (such as the get_ancestors () method inside), by default the Page class (in this case, not TopPage, but that) It seems that an instance of (superclass) is referenced. Therefore, as it is, the attributes added in the subclass cannot be accessed, but the trick is to add .specific to make it possible.

Next, if you look at the code of the side_bar block and footer block in the main block, you can see that the necessary information is obtained from the corresponding TopPage instances and rendered.

Finally, let's move on to the nav_bar block. The contents of this block are empty because we let React and React Bootstrap render this <div id =" nav_bar ">. The specific script is described in the extra_js block. You can also confirm that the nav_items prepared above is used and that the URL of the linked page can be obtained with the pageurl tag of Wagtail.

Note that js/navbar.min.js read at the beginning of this block is a compilation/compression of the following React code created using JSX.

var Navbar = ReactBootstrap.Navbar;
var Nav = ReactBootstrap.Nav;

function MyNavbar (props) {
  return (
    <Navbar bg={props.color} variant={props.variant} fixed="top" expand="sm">
      <Navbar.Brand href={props.brandhref}>{props.brandlabel}</Navbar.Brand>
      <Navbar.Toggle/>
      <Navbar.Collapse>
      <Nav className="ml-auto">
        {props.navs.map((nav) => (
          <Nav.Link href={nav.href}>{nav.label}</Nav.Link>
        ))}
      </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
};

Now you can safely display the pages of the PlainPage class.

Finally, since the TopPage class has been extended above, I would like to modify the template templates/cms/top_page.html there as well. In fact, you don't have to add a lot of code to this file for that. Rather, all you have to do is greatly simplify it as shown below.

{% extends 'cms/plain_page.html' %}

That is, the template for the PlainPage class can be used as it is for the page of the TopPage class. Yes, that's why we added the seemingly useless get_top_page () method to the TopPage class.

in conclusion

This time, after understanding that the pages under the control of Wagtail are organized in a tree structure, I touched on a part of the technique to use it. I also tried to define page parts using the Orderable class.

Next time, I'd like to add the ListPage class and touch on some techniques related to it.

Link

-Wagtail Recommendations (1) A story about choosing Wagtail by comparing and examining Django-based CMS -Wagtail Recommendation (2) Let's define, edit, and display the page -Wagtail Recommendation (4) Let's pass context to template -Wagtail Recommendation (5) Let's add your own block to StreamField -Wagtail Recommendation (6) Let's add categories and tags

Recommended Posts

Wagtail Recommendations (3) Understand and use the tree structure of pages
Wagtail Recommendations (2) Let's define, edit, and display a page
Wagtail Recommendations (3) Understand and use the tree structure of pages
Review the tree structure and challenge BFS
Understand the Decision Tree and classify documents
How to use machine learning for work? 01_ Understand the purpose of machine learning
[Required subject DI] Implement and understand the mechanism of DI with Go
The story of Python and the story of NaN
[Python / matplotlib] Understand and use FuncAnimation
Understand the contents of sklearn's pipeline
Make a decision tree from 0 with Python and understand it (4. Data structure)
This and that of the inclusion notation.
Understand the benefits of the Django Rest Framework
[Python3] Understand the basics of Beautiful Soup
Output tree structure of files in Python
I summarized the folder structure of Flask
Review the concept and terminology of regression
[Python] Understand the content of error messages
Basics: Full Search of Tree Structure (DFS/BFS)
Understand the "temporary" part of UNIX / Linux
Reviewing the wooden structure and challenging DFS
The story of trying deep3d and losing
[Python3] Understand the basics of file operations