Andy Pestell
Andy Pestell

Reputation: 58

Rendering ForeignKey objects in template with Django

I'm having trouble rendering related objects in a template. I have a Page model, that has a ForeignKey relationship to itself to define parent-child relationships.

class Page(BaseModel):
    title = models.CharField(
        max_length=280,
        blank=False,
        null=False,
    )
    description = models.CharField(
        max_length=280,
        blank=False,
        null=False,
    )
    slug = models.SlugField(
        blank=False,
        null=False,
    )
    is_home = models.BooleanField(
        default=False,
    )
    is_parent = models.BooleanField(
        default=False,
    )
    parent = models.ForeignKey(
        'self',
        on_delete=models.PROTECT,
        default='Home',
        blank=True,
        null=True,
        related_name='children',
        )
    content = RichTextField(
        blank=False,
        null=False,
    )

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('page_detail', args=[str(self.slug)])

My views.py filters out non-parent pages:

class PageListView(ListView):
    queryset = Page.objects.filter(is_parent=True, is_home=False)
    template_name = 'pages/page_list.html'
    context_object_name = 'page'

But when it comes to rendering 'child' objects in my template, I'm stuck. I've figured that I need a loop within a loop (first for parent pages, second for child pages per parent page), but can't get the second loop to work. Here was my latest attempt, which got me a "'Page' object is not iterable" error.

{% extends '_base.html' %}

{% block content %}
<div class="container">
    {% for page in page %}
        <p>{{ page.title }}</p>
           {% for children in page %}
              <p>{{ page.title }}</p>
           {% endfor %}
    {% endfor %}
</div>
{% endblock content %}

Upvotes: 1

Views: 259

Answers (1)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 477338

You can iterate over the manager constructed by the related_name:

{% block content %}
<div class="container">
    {% for page in page %}
        <p>{{ page.title }}</p>
           {% for child in page.children.all %}
              <p>{{ child.title }}</p>
           {% endfor %}
    {% endfor %}
</div>
{% endblock content %}

You can boost efficiency with a .prefetch_related(…) [Django-doc] clause:

class PageListView(ListView):
    queryset = Page.objects.filter(
        is_parent=True, is_home=False
    ).prefetch_related('children')
    template_name = 'pages/page_list.html'
    context_object_name = 'page'

I would furthermore advise not to create an extra field is_parent, since this is a form of data duplication. It turns out that keeping fields in sync, even on the same database, might be harder than what it appears to be at first sight. You can check if the object is a parent with:

class PageListView(ListView):
    queryset = Page.objects.filter(
        children__isnull=False, is_home=False
    ).distinct().prefetch_related('children')
    template_name = 'pages/page_list.html'
    context_object_name = 'page'

Upvotes: 2

Related Questions