Search code examples
pythondjangowagtail

Wagtail: How to filter children of index page, when index page is accessed through foreign key


I apologize for any mistakes, English is not my native language.

I am making site with news and events of multiple organizations (each organization has their own account allowing them to create news and events by themself).

Using the news as an example:

NewsIndexPage is parent for all NewsArticlePages. Each NewsArticlePage is linked with organization.

At url /news/ NewsIndexPage shows all news and allow to filter news by organization with RoutablePageMixin (/news/organization/<str: organization_slug>/).

Also each organization has their own landing page (OrganizationPage) with organization's info and news section. News section shows up to 3 latest news of organization (like in bakerydemo repository, when latest 3 blog entries is listed in HomePage:

Each OrganizationPage references NewsIndexPage through ForeignKey.

But I don't know, how to filter children of NewsIndexPage when it accessed through ForeignKey.

When I access OrganizationPage instance I want to pass organization's slug to linked NewsIndexPage so I can filter children NewsArticlesPages.

The only thing I came up with is to create template_tag that will pass slug as an argument to some kind of get_news_articles(self, organization_slug) function. But I haven't been able to do that yet.

news.models:

class NewsArticlePage(Page):
    """
    A news article page.
    """
    image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        help_text=_("Landscape mode only; horizontal width between 1000px and 3000px."),
    )
    body = StreamField(
        BaseStreamBlock(), verbose_name=_("News article page body"), blank=True, use_json_field=True
    )
    date_published = models.DateField(_("Date article published"), blank=True, null=True)
    organization = models.ForeignKey(
        to = Organization,
        on_delete = models.SET_NULL,
        related_name = "news_articles",
        blank=True,
        null=True,
    )

    content_panels = Page.content_panels + [
        FieldPanel("introduction"),
        FieldPanel("image"),
        FieldPanel("body"),
        FieldPanel("date_published"),
    ]
    private_panels = [
        FieldPanel("organization"),
    ]

    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading=_("Details")),
        ObjectList(private_panels, heading=_("Admin only"), permission="superuser"),
    ])

    search_fields = Page.search_fields + [
        index.SearchField("body"),
    ]

    # Specifies parent to NewsArticlePage as being NewsIndexPages
    parent_page_types = ["NewsIndexPage"]

    # Specifies what content types can exist as children of NewsArticlePage.
    # Empty list means that no child content types are allowed.
    subpage_types = []

    base_form_class = NewsArticlePageForm

    class Meta:
        verbose_name = _("News article page")
        verbose_name_plural = _("News article pages")


class NewsIndexPage(RoutablePageMixin, Page):
    """
    Index page for news.
    """

    introduction = models.TextField(help_text=_("Text to describe the page"), blank=True)

    content_panels = Page.content_panels + [
        FieldPanel("introduction"),
    ]

    def get_context(self, request):
        context = super(NewsIndexPage, self).get_context(request)
        context["news_articles"] = (
            NewsArticlePage.objects.live().order_by("-date_published")
        )
        return context

    def children(self):
        return self.get_children().specific().live()
    
    @route(r"^organization/$", name="news_of_organization")
    @path("organization/<str:organization_slug>/", name="news_of_organization")
    def news_of_organization(self, request, organization_slug=None):
        """
        View function for the organization's news page
        """
        try:
            organization = Organization.objects.get(slug=organization_slug)
        except Organization.DoesNotExist:
            if organization:
                msg = "There are no news articles from organization {}".format(organization.get_shorten_name())
                messages.add_message(request, messages.INFO, msg)
            return redirect(self.url)
        
        news_articles = NewsArticlePage.objects.live().filter(organization__slug=organization_slug).order_by("-date_published")

        return self.render(request, context_overrides={
            'title': _("News"),
            'news_articles': news_articles,
        })


    def serve_preview(self, request, mode_name):
        # Needed for previews to work
        return self.serve(request)
    

    subpage_types = ["NewsArticlePage"]


    class Meta:
        verbose_name = _("News index page")
        verbose_name_plural = _("News index pages")

organizations. Models:

class Organization(index.Indexed, models.Model):
    # ...
    name = models.CharField(max_length=255)
    # ...
    slug = models.SlugField(
        verbose_name=_("Slug Name"),
        max_length=255,
        unique=True,
        blank=True,
        null=False,
    )
    # ...


class OrganizationPage(Page):
    """
    An organization page.
    """
    organization = models.OneToOneField(
        to=Organization,
        on_delete=models.SET_NULL,
        related_name="organization_page",
        verbose_name=_("Organization"),
        null=True,
        blank=True,
    )

    # Featured sections on the HomePage
    # News section
    news_section_title = models.CharField(
        blank=True, max_length=255, help_text=_("Title to display")
    )
    news_section = models.ForeignKey(
        "news.NewsIndexPage",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        help_text=_(
            "News section. Will display up to three latest news articles."
        ),
        verbose_name=_("News section"),
    )

    content_panels = Page.content_panels + [
        MultiFieldPanel(
            [
                FieldPanel("news_section_title"),
                FieldPanel("news_section"),
            ],
            heading=_("News section"),
        ),
    ]

    private_panels = [
        FieldPanel("organization"),
    ]

    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading=_("Details")),
        ObjectList(Page.promote_panels, heading=_("Promote")),
        ObjectList(private_panels, heading=_("Admin only"), permission="superuser"),
    ])

    # Specifies parent to OrganizationPage as being OrganizationsIndexPage
    parent_page_types = ["OrganizationsIndexPage"]

    # Specifies what content types can exist as children of OrganizationPage.
    # subpage_types = ["news.NewsIndexPage"]
    subpage_types = []

    class Meta:
        verbose_name = _("Organization page")
        verbose_name_plural = _("Organization pages")

templates/organizations/organizations/organization_page.html:

<!-- ... -->
<div class="container">
        <div class="row">
            <div class="news-articles-list">
                {% if page.news_section %}
                    <h2 class="featured-cards__title">{{ page.news_section_title }}</h2>
                    <div class="row">
                        {% for news_article in page.news_section.children|slice:"3" %}
                            {% include "includes/card/news-listing-card.html" with news_article=news_article %}
                        {% endfor %}
                    </div>
                    <a class="featured-cards__link" href="/news/organization/{{ page.slug }}">
                        <span>View more of our news</span>
                    </a>      
                {% endif %}
            </div>
        </div>
    </div>
<!-- ... -->

I would be grateful for any help and tips!


Solution

  • Don't try to filter the query inside the template - do it in a get_context method on the page model:

    class OrganizationPage(Page):
        # ...
        def get_context(self, request, *args, **kwargs):
            context = super().get_context(request, *args, **kwargs)
            context['news_articles'] = NewsArticlePage.objects.child_of(self.news_section).filter(organization=self.organization).live()[:3]
            return context
    

    This will make a variable news_articles available on the template, which you can loop over:

    {% for news_article in news_articles %}
        {% include "includes/card/news-listing-card.html" with news_article=news_article %}
    {% endfor %}