2

I'm trying to setup a Wagtail site with an article to pages structure but I'm struggling. A review article for example may have an introduction page, a benchmark page and a conclusion page. I want to work out how to allow this relationship in wagtail and have it so that editors can add multiple pages to the same article on the same page. I can imagine the pages interface looking a bit like how you have content, promote and settings on pages but with the ability to add, rename and reorder pages. I've tried using a foreign key on a page model that links to an article but I can't get it to be shown in the admin the way I want.

Here is the django version of model layout I was looking to use. You have a parent article that is then made up of one or multiple pages. The pages should be editable, orderable and be created from within one panel in the admin with streamfields:

Class Article(models.Model)
    STATE_DRAFT = 0
    STATE_REVIEW= 1
    STATE_PUBLICATION = 2
    STATE_HIDDEN = 3
​
    STATE = (
        (STATE_DRAFT, 'draft'),
        (STATE_REVIEW, 'pending review'),
        (STATE_PUBLICATION, 'ready for publication'),
        (STATE_HIDDEN, 'hide and ignore'),
    )
    title = models.CharField(_('title'), max_length=256)
    slug = models.SlugField(
        _('slug'), unique=True, blank=True, default='', max_length=256
    )
    description = models.TextField(
        _('description'), max_length=256, blank=True, default=''
    )
    author = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name='article'
    )
    publication = models.DateTimeField(
        null=True, blank=True, default=None, db_index=True, help_text='''
            What date and time should the article get published
        '''
    )
    state = models.PositiveIntegerField(
        default=0, choices=STATE, help_text='What stage is the article at?'
    )
    featured = models.BooleanField(
        default=False,
        help_text='Whether or not the article should get featured'
    )
​
class Page(Page):
    article = models.ForeignKey(
        'Article', on_delete=models.CASCADE, related_name='pages'
    )
    title = models.CharField(max_length=256)
    number = models.PositiveIntegerField(default=1) # So pages are ordered
    body = models.TextField(blank=True)
Max Loyd
  • 408
  • 6
  • 21
  • Can you share some of your code? Also is `Article` a subclass of `Page`? Or is it some other class? – Nico Griffioen Oct 03 '19 at 15:09
  • @NicoGriffioen I have added a code snippet of the model layout I was going for. Maybe you can understand from that. – Max Loyd Oct 03 '19 at 15:29
  • I'm having a bit of a hard time getting my head around what you're trying to achieve here. Basically what you're saying is, a Page is linked to an Article, and there can be multiple pages displayed for one article. But what is the role of this Article in your Wagtail? What exactly are you trying to achieve, and why do you not make Article a subclass of Wagtail's default Page? – Nico Griffioen Oct 03 '19 at 15:34
  • @NicoGriffioen Essentially the Article class contains no content. It just has meta data about the article such as title, snippet and thumbnail. This is the information shown in list views and on RSS feeds. The Page model is where the content of each page is stored along with a number so the pages are ordered, and a title. Here is an example of it implemented. https://www.techradar.com/uk/reviews/huawei-p30-pro. See how they have a base article that then has 5 pages – Max Loyd Oct 03 '19 at 15:40
  • To be honest, it sounds like you're proposing to reinvent so much of Wagtail's native feature set (from the admin UI, through the publishing workflow, right down to the whole concept of the page tree) that there'll be very little of Wagtail left by the time you're done. Ultimately I think you'll have to choose between compromising on some of this architecture in favour of something that fits Wagtail's model more closely, or implementing a bespoke Django CMS. – gasman Oct 03 '19 at 18:12

2 Answers2

4

As per my comment I don't think you'll be able to achieve everything you're looking for short of implementing an entirely bespoke CMS - but if you're able to bend the UI and data modelling requirements, then Wagtail's RoutablePageMixin is one possible way of achieving the general pattern of editing an article as a single unit, while presenting it as multiple pages on the front-end.

In this approach, you'd make Article a Wagtail Page model, with all of the sub-page content defined as fields (or InlinePanel child models) on that model. (If you want to split the content entry into tabs within the editing interface, see Customising the tabbed interface, although this won't support dynamically adding / reordering them.) You'd then define a URL route and template for each subpage of the article:

from wagtail.core.models import Page
from wagtail.contrib.routable_page.models import RoutablePageMixin, route


class ArticlePage(RoutablePageMixin, Page):
    intro = StreamField(...)
    main_page = StreamField(...)
    conclusion = StreamField(...)

    @route(r'^$')
    def intro_view(self, request):
        render(request, 'article/intro.html', {
            'page': self,
        })

    @route(r'^main/$')
    def main_page_view(self, request):
        render(request, 'article/main_page.html', {
            'page': self,
        })

    @route(r'^conclusion/$')
    def conclusion_view(self, request):
        render(request, 'article/conclusion.html', {
            'page': self,
        })

In this example the three sub-pages are hard-coded, but with some more work (perhaps an InlinePanel child model with a slug field and a StreamField) you could make the subpages dynamic.

gasman
  • 23,691
  • 1
  • 38
  • 56
3

I saw gasman already provided an answer to you question, but I'm still going to write up an answer for two reasons:

  • I think you need some more pointers as to why gasmans' proposal is a better solution than yours, but it's way to much to write in a comment.

  • I have implemented a similar solution before, where there is a top level 'Article'-like object with multiple reorderable child objects, where the actual content resides.

Why you should make Article a Page subclass

You chose not to make Article a subclass of Page, and you said it was because the Article itself does not contain any content, only metadata about an article. That is not a very strange thought process, but I think you're looking at the wrong requirements for your Article model.

Let's look at Wagtail's own Page model. What kind of functionality does it provide out of the box?

  • It provides a tree structure with parent and child pages, so that your page can be placed somewhere in the hierarchy of your website
  • It provides a slug_field, so that Wagtail can automatically handle linking to your page.
  • It provides functionality for drafting, publishing and unpublishing.

Wagtail doesn't dictate anything about content, leaving you to decide what kind of content you want to put on your Page subclass, if any. Examples of Pages that do not have a body would be:

  • Contact forms.
  • Blog index pages.

Good questions you could ask when deciding whether you want a Model to be a subclass of a Page are:

  • Do I want this object to have it's own url?
  • Do I want to be able to place this object somewhere inside my website hierarchy?
  • Do I want to have SEO advantages for the object?
  • Do I want to be able to publish/unpublish this object or not?

In your case of the Article, you could say yes to almost al these question, so it'd be wise to make it a Page subclass. That way, you don't have to reinvent the wheel.

How you define the actual 'body' of your page is up to you. You can place the actual content in either snippets, or subpages to that Article. Or you can even choose to create a list of StreamFields inside your model.

How to implement ordered subcontent.

I have implemented a structure like this before. The way I did this was very similar to what gasman proposes.

In my case, I needed to create a website where you could find an object (like you article) and display different types of explanation modules for it. For each document, I created a ArticlePage, and for each explanation module, I created a snippet called ExplanationModule.

I then created a through model with an ordering, and added a RoutablePageMixin to the class like gasman explains.

The structure looks something like this:

@register_snippet
class ArticlePageModule(models.Model):
    ...

    title = models.CharField(max_length=100)
    body = StreamField(LAYOUT_STREAMBLOCKS, null=True, blank=True)

    panels = [
        FieldPanel('title'),
        StreamFieldPanel('body'),
    ]

class ArticlePageModulePlacement(Orderable, models.Model):
    page = ParentalKey('articles.ArticlePage', on_delete=models.CASCADE, related_name='article_module_placements')

    article_module = models.ForeignKey(ArticlePageModule, on_delete=models.CASCADE, related_name='+')

    slug = models.SlugField()

    panels = [
        FieldPanel('slug'),
        SnippetChooserPanel('article_module'),
    ]

class ArticlePage(Page, RoutablePageMixin):
    # Metadata and other member values
    ....

    content_panels = [
    ...
    InlinePanel('article_module_placements', label="Modules"),
    ]

    @route(r'^module/(?P<slug>[\w\-]+)/$')
    def page_with_module(self, request, slug=None):
        self.article_module_slug = slug
        return self.serve(request)


    def get_context(self, request):
        context = super().get_context(request)

        if hasattr(self, 'article_module_slug'):
            context['ArticlePageModule'] = self.article_module_placements.filter(slug = self.article_module).first().article_module

        return context

What this does is the following:

  • Create a ArticlePageModule snippet, which is just some kind of content, like a title and a body.

  • Create a ArticlePageModulePlacement which links a ArticlePage to a module, and adds the following:

    • A slug
    • An Ordering (Because it subclasses the Orderable mixing)
  • Create a ArticlePage which does two things:

    • Define a ArticlePageModuleplacement panel, which allows you to add ArticlePageModulePlacements
    • Subclass RoutablePagemixin, as described in gasman's answer.

This provides you with a Wagtail-proof, reusable and robust way of creating Articles with SubContent. The modules don't show up in tabs, but will be shown on the page's layout page under a panel called 'Modules'.

Nico Griffioen
  • 5,143
  • 2
  • 27
  • 36
  • Thank you for that, will take both solutions into account and decide what one will work best. – Max Loyd Oct 04 '19 at 13:47
  • Hey Nico, thanks so much for the detailed write-up! I'm a wagtail beginner feeling really frustrated to understand why and when I should use Page instead of Django models, not found much doc yet. In your answer, you said **It provides functionality for drafting, publishing and unpublishing.**, I don't understand why this is a benefit of using Page, can't we also use these features (i.e. panels or StreamField or RichTextField) in Django models? – avocado Jun 25 '20 at 16:27
  • Also you said ** You can place the actual content in either snippets**, I wonder is this a valid or an appropriate use-case for snippets to store data? Since to me, snippets are used to store simple data related to the website. For example, I want to build an ecommerce website, there could be a lot of orders generated, and I think orders could be defined as Django models, not snippets. – avocado Jun 25 '20 at 17:42
  • BTW, here is the question I asked on SO https://stackoverflow.com/q/62581737/2235936 – avocado Jun 25 '20 at 18:11