django/wagtail – object attribute showing None when admin panel states otherwise?

I’m having a problem understanding why my {{ post.categories }} in templates is showing itself with blog.PostPageBlogCategory.None when the admin panel shows a chosen category for the post object.

Here is my model.py set up:

from django.db import models

# Create your models here.
from django.db import models

from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager

from taggit.models import Tag as TaggitTag
from taggit.models import TaggedItemBase

from wagtail.admin.edit_handlers import (
    FieldPanel,
    FieldRowPanel,
    InlinePanel,
    MultiFieldPanel,
    PageChooserPanel,
    StreamFieldPanel,
)
from wagtail.core.models import Page
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.snippets.edit_handlers import SnippetChooserPanel
from wagtail.snippets.models import register_snippet


class BlogPage(Page):
    description = models.CharField(max_length=255, blank=True,)

    content_panels = Page.content_panels + 
        [FieldPanel("description", classname="full")]


class PostPage(Page):
    header_image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    tags = ClusterTaggableManager(through="blog.PostPageTag", blank=True)

    content_panels = Page.content_panels + [
        ImageChooserPanel("header_image"),
        InlinePanel("categories", label="category"),
        FieldPanel("tags"),
    ]


class PostPageBlogCategory(models.Model):
    page = ParentalKey(
        "blog.PostPage", on_delete=models.CASCADE, related_name="categories"
    )
    blog_category = models.ForeignKey(
        "blog.BlogCategory", on_delete=models.CASCADE, related_name="post_pages"
    )

    panels = [
        SnippetChooserPanel("blog_category"),
    ]

    class Meta:
        unique_together = ("page", "blog_category")


@register_snippet
class BlogCategory(models.Model):

    CATEGORY_CHOICES = (
        ('fighter', 'Fighter'),
        ('model', 'Model'),
        ('event', 'Event'),
        ('organization', 'Organization'),
        ('other', 'Other')
    )

    name = models.CharField(max_length=255)
    slug = models.SlugField(unique=True, max_length=80)

    category_type = models.CharField(
        max_length=100, choices=CATEGORY_CHOICES,  blank=True)

    description = models.CharField(max_length=500, blank=True)

    panels = [
        FieldPanel("name"),
        FieldPanel("slug"),
        FieldPanel("category_type"),
        FieldPanel("description"),
    ]

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = "Category"
        verbose_name_plural = "Categories"


class PostPageTag(TaggedItemBase):
    content_object = ParentalKey("PostPage", related_name="post_tags")


@register_snippet
class Tag(TaggitTag):
    class Meta:
        proxy = True

A little context on my blog_page.html template. This template is a list of all posts created in a blog, not the individual posts themselves.

{% extends "base.html" %} 

{% load wagtailcore_tags wagtailimages_tags %} 

{% block content %}


<section class="text-gray-600 body-font">
  <div class="container px-5 py-24 mx-auto">
    <div class="flex flex-wrap -m-4">
      {% for post in page.get_children.specific %}

      <div class="p-4 md:w-1/3">
        <div
          class="
            h-full
            border-2 border-gray-200 border-opacity-60
            rounded-lg
            overflow-hidden
          "
        >
          {% if post.header_image %} {% image post.header_image original as header_image %}
          <a href="{% pageurl post %}">
            <img
              src="{{ header_image.url }}"
              class="lg:h-48 md:h-36 w-full object-cover object-center"
            />
          </a>
          {% endif %}
          <div class="p-6">


            <h2
              class="
                tracking-widest
                text-xs
                title-font
                font-medium
                text-gray-400
                mb-1
              "
            >
              {{ post.categories}}

            </h2>
       ## The rest omitted for brevity

Now, I’m able to extract data from post object, such as date, title, image, but for some reason with the way I set up my categories in models.py, I’m not able to present the correct category for each post.

Here is an image of the template and admin for more context:

enter image description here enter image description here

As you see blog.PostPageBlogCategory.None is showing itself for {{post.categories}}. Ideally, the correct Category shown should be a string object, not None.

What did I do wrong with my models?

Answer

This is a Django quirk… post.categories does not give you the list of categories itself, but a manager object providing various operations on that relation. (I’m not sure why the string representation of that manager object comes up as "blog.PostPageBlogCategory.None", though…)

post.categories.all will give you the actual queryset of PostPageBlogCategory objects, but since you haven’t provided an __str__ method on that class, outputting that directly probably won’t show anything meaningful either. Looping over it should give you what you’re looking for:

{% for post_category in post.categories.all %}
    {{ post_category.blog_category }}
{% endfor %}