JustBlossom
JustBlossom

Reputation: 1329

Create custom url wagtail

I have a site that I've structured like so:

Top Page: Animals
Subpage: Cat
Subpage: Dog

Top Page: Cat
Subpage: Food

Top Page: Dog
Subpage: Food

That way, a user can go to site/animals or site/cat. Animals is high-level info, Cat and Dog are more detailed parts of the site.

What I want to do:
Occasionally, I want to be able to reuse pages from Animals and use them in Cat or Dog. But I don't want the url to switch from one section to another.

Example: If the user is under Animal, and they click a food article, the url should be: animals/cats/food_article

If they click on that same article under cats, the url should look like: cats/food_article

What I've tried:
I've tried using RoutableRouteMixin. But that works for subpages, not for pages at the same level.

I tried overwriting the get_url_parts method for the article models. But then I got 404 errors because the pages didn't actually exist at the url I created.

Can this be achieved in Wagtail? Or is there a Django solution I can use with Wagtail?

Upvotes: 0

Views: 975

Answers (2)

LB Ben Johnston
LB Ben Johnston

Reputation: 5176

Potential Approach

  • Without major customisation, you will need to add a separate Page model for your articles, these can be added under the root page and then just not shown by default (including hidden from any menu setup you have).
  • Then create a modelcluster that will be the relation between your animal pages (cat, dog) and your Article pages.
  • Finally, override the Animal page's get_children method which is on each Page and comes from django-treebeard. This method is used by the routing logic to determine if a page is a child of another (when a URL is read).
  • Once you create a few AnimalPages, then create an ArticlePage (e.g. Food Article), then go into the dog page AND cat page and link the food article to the page.
  • At this point you will be able to access the food-article at 3 URLs; http://localhost:8000/food-article/, http://localhost:8000/animals/cats/food-article/ and http://localhost:8000/animals/dogs/food-article/.
  • You can make the original food-article URL not work a few different ways, but the simplest way would be to put all ArticlePages under their own parent page and then use routablemixin to make that page not show any sub-urls.
    • Note: If you put all articles under their own subpage (e.g. ArticlesPage, this should still work as it only checks the 'last' section of the URL). I have not tested this though.

Example Code

from django.db import models

from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel

from wagtail.admin.edit_handlers import (
    InlinePanel,
    PageChooserPanel,
)

from wagtail.core.models import Orderable, Page

class ArticlePage(Page):
    # Articles will be available to link to any other AnimalPage and content shared

    content_panels = Page.content_panels

    subpage_types = [] # no sub-pages allowed

class RelatedArticle(models.Model):
    # http://docs.wagtail.io/en/v2.7.1/reference/pages/panels.html#inline-panels
    article_page = models.ForeignKey(
        'wagtailcore.Page',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
    )

    panels = [
        PageChooserPanel('article_page', 'base.ArticlePage')
    ]

    class Meta:
        abstract = True

class ArticlePageRelatedPages(Orderable, RelatedArticle):
    page = ParentalKey(
        'base.AnimalPage',
        on_delete=models.CASCADE,
        related_name='related_articles'
    )

class AnimalsPage(Page):
    # This is our root animals page

    content_panels = Page.content_panels

    subpage_types = ['AnimalPage']


class AnimalPage(Page):

    def get_articles(self):
        # returns a queryset (not a list) of all Pages that are linked articles
        return Page.objects.filter(id__in=[
            r.article_page.pk for r in self.related_articles.all()
        ])

    def get_children(self):
        # override the method from django-treebeard
        # can be used in the template or wherever needed for 'children'
        # this method is also used when attempting to find child urls
        sub_pages = super().get_children()
        articles = self.get_articles()
        return articles | sub_pages # merges the two querysets for the 'or' operator

    content_panels = Page.content_panels + [
        InlinePanel('related_articles', label='Related Articles'),
    ]

    subpage_types = [] # no sub-pages allowed

Upvotes: 1

david
david

Reputation: 2171

You could create IncludePage as below and use it to present the same content under a different url.

from django.db import models
from wagtail.core.models import Page
from wagtail.admin.edit_handlers import PageChooserPanel
from django.http import Http404


class IncludePage(Page):
    page  = models.ForeignKey('wagtailcore.Page',
                              null=True, blank=True,
                              on_delete=models.SET_NULL,
                              related_name='+')

    content_panels = Page.content_panels + [
              PageChooserPanel('page'),
             ]

    def get_template(self, *args, **kwargs):
        page = self.page.specific
        if type(page) == IncludePage:
            raise Http404("Avoid recursion")
        return page.get_template(*args, **kwargs)

    def get_context(self, *args, **kwargs):
        page = self.page.specific
        if type(page) == IncludePage:
            raise Http404("Avoid recursion")
        return page.get_context(*args, **kwargs)

Note however this won't work for pages with customized serve functions (e.g. pages using RoutableRouteMixin). For those you would have to duplicate the serve functionality across in the IncludePage to match. But depending on your website this might be useful.

Upvotes: 1

Related Questions