Add a Categories system to Getting Started tutorial

pull/3150/head
Scot Hacker 2016-11-02 15:01:41 -07:00 zatwierdzone przez Matt Westcott
rodzic 8c5b62ccfc
commit 3634c2b289
6 zmienionych plików z 241 dodań i 7 usunięć

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 20 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 51 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 48 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 27 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 358 KiB

Wyświetl plik

@ -444,14 +444,17 @@ You can read more about using images in templates in the
:doc:`docs <../topics/images>`.
Tagging Posts
~~~~~~~~~~~~~
Tags and Categories
~~~~~~~~~~~~~~~~~~~
Let's say we want to let editors "tag" their posts, so that readers can, e.g.,
view all bicycle-related content together. For this, we'll need to invoke
the tagging system bundled with Wagtail, attach it to the ``BlogPage``
model and content panels, and render linked tags on the blog post template.
Of course, we'll need a working tag-specific URL view as well.
What's a blog without a solid taxonomy? You'll probably want Categories
for "big picture" taxonomy ("News," "Sports," "Politics," etc.) and Tags
for fine-grained sorting ("Bicycle," "Clinton," "Electric Vehicles," etc.)
You'll need mechanisms to let editors manage tags categories and attach them to posts,
ways to display them on your blog pages, and views that display all posts belonging
to a given tag or category.
Let's start with tags, since they're bundled with Wagtail.
First, alter ``models.py`` once more:
@ -606,6 +609,237 @@ something like this:
.. figure:: ../_static/images/tutorial/tutorial_9.png
:alt: A simple tag view
Categories
~~~~~~~~~~
Now to add a Categories system. Again, alter ``models.py``:
.. code-block:: python
class BlogCategory(models.Model):
name = models.CharField(max_length=256)
slug = models.CharField(max_length=12)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Blog Categories"
This model does *not* subclass the Wagtail ``Page``
model, and is *not* a Wagtail Snippet - it's a standard Django model! While we could have created
categories as Pages, that wouldn't really make a lot of sense - while we'll eventually
want pages for our categories, a category itself is more of a metadata storage structure than a page,
so it makes sense to make it a vanilla Django model. As a result, this exercise will also show
how to integrate non-Wagtail models into the Wagtail workflow.
As an aside, the ``BlogCategory`` model could easily live in a totally different app of your
Django project, and just be imported normally into your Wagtail blog app. This would be important if you were, e.g.,
integrating a Wagtail blog into a pre-existing Django site that already had a system of categories.
We want to create a ManyToMany relationship between BlogCategory and BlogPage. In standard Django, we would do
something like this:
``categories = models.ManyToManyField(BlogCategory, blank=True)``
However, it's a bit trickier than that with Wagtail because of the ``modelcluster`` dependency it
uses to maintain hierarchical relationships. ``modelcluster`` is at the heart of Wagtail, but does not
support M2M relationships. Instead, we'll need to define the related table manually:
.. code-block:: python
class BlogCategoryBlogPage(Orderable, models.Model):
category = models.ForeignKey(BlogCategory, related_name="+")
page = ParentalKey(BlogPage, related_name='blog_categories')
This model's table will store relationships between blog pages and the categories assigned to them,
effectively giving us the equivalent of a ManyToMany relationship. For readability, we named the class
by concatenating the names of the two related models. The class also subclasses ``Orderable``,
which means you'll be able to control the order of Categories on a blog post via the Wagtail admin.
Now we just need to attach a "panel" for the relationship to our BlogPost. In the ``BlogPost`` model,
add an ``InlinePanel`` for the "related_name" ``blog_categories:``
.. code-block:: python
content_panels = Page.content_panels + [
FieldPanel('date'),
ImageChooserPanel('main_image'),
FieldPanel('intro'),
FieldPanel('body'),
InlinePanel('blog_categories', label="Blog Categories"),
MultiFieldPanel([
FieldPanel('tags'),
], heading="Tags"),
]
Run ``python manage.py makemigrations`` and ``python manage.py migrate,`` then view an admin page for a ``BlogPage:``
.. figure:: ../_static/images/tutorial/tutorial_10.png
:alt: A category picker for BlogPage
At first, we have no categories to choose from. Unlike the Django admin, we can't add them on the fly from here.
Since we didn't create ``BlogCategory`` as a Page or Snippet, Wagtail isn't automatically aware of it, so we'll
need to expose it in the admin manually. Fortunately, Wagtail provides a mechanism for this,
via ``ModelAdmin``. Create a new file in your blog app, called ``wagtail_hooks.py:``
.. code-block:: python
from wagtail.contrib.modeladmin.options import (ModelAdmin, modeladmin_register)
from blog.models import BlogCategory
class BlogCategoryAdmin(ModelAdmin):
model = BlogCategory
add_to_settings_menu = True
list_display = ('name', 'slug')
modeladmin_register(BlogCategoryAdmin)
``wagtail_hooks`` lets you control aspects of the admin, and to expose non-Wagtail models.
In this example, we've specified:
``add_to_settings_menu = True``
So that our BlogCategories appear in the global Settings menu:
.. figure:: ../_static/images/tutorial/tutorial_11.png
:alt: Adding Blog Categories to Settings
.. figure:: ../_static/images/tutorial/tutorial_12.png
:alt: Categories listing
After using your new Blog Categories interface to create some categories, you can select them from the
InlinePanel in a BlogPage:
.. figure:: ../_static/images/tutorial/tutorial_13.png
:alt: Newly created categories available to a BlogPage
Now that we're storing categories on posts, we need a view to display them, and a way to link to them.
Rather than create another model for the new view, let's consider a category to be a "slice" of data exposed
on the ``BlogIndexPage.`` We can pass a category to the view either as URL parameter: ``/blog?cat=science``
or as a keyword on the end of the URL, which is much cleaner: ``/blog/science``. To access that keyword, we'll
take advantage of Wagtail's :doc:`RoutablePageMixin <../reference/contrib/routablepage>` class. Modify
``BlogIndexPage`` like this:
.. code-block:: python
from wagtail.contrib.wagtailroutablepage.models import RoutablePageMixin, route
from django.shortcuts import get_object_or_404, render
class BlogIndexPage(RoutablePageMixin, Page):
intro = RichTextField(blank=True)
def get_context(self, request):
# Update context to include only published posts, ordered by reverse-chron
context = super(BlogIndexPage, self).get_context(request)
blogpages = self.get_children().live().order_by('-first_published_at')
# Include queryset of non-empty blog categories for menu
usedcats = BlogCategoryBlogPage.objects.distinct().values_list('category__slug', flat=True)
blogcats = BlogCategory.objects.filter(slug__in=usedcats)
context['blogpages'] = blogpages
context['blogcats'] = blogcats
return context
@route(r'^cat/(\w+)/$', name="blog_category")
def category(self, request, catslug=None):
"""
Filter BlogPages by category
"""
category = get_object_or_404(BlogCategory, slug=catslug)
blogpages = BlogPage.objects.filter(
blog_categories__category=category).live().order_by('-first_published_at')
context = self.get_context(request)
context['blogpages'] = blogpages
context['category'] = category
return render(request, 'blog/blog_index_page.html', context)
The ``@route`` decorator is new, but as you can see, it works pretty much the same as standard Django URLs,
with a regex pattern matcher and a route name, which we'll use in a minute. The ``^cat...`` in the regex
matches a URL pattern starting at the parent page, so in this case we're matching e.g.
``/blog/cat/science.`` We query for a ``BlogCategory`` object (or 404), then use it to filter ``BlogPage``
records, traversing through our "ManyToMany" table. Note that when using ``route,``
we need to call Django's ``render()`` manually, specifying the template name.
Since we want to display a nav menu including all non-empty categories, we also insert that queryset
into the context (notice how the ``category()`` suburl calls ``get_context()`` before
appending to context, so the categories list is available on all blog index views.)
Assuming you've created a "Science" category and added some posts to that category, you should now be
able to access a URL like ``/blog/cat/science.`` Now we just need to add category links to our index
and post templates.
We'll also need to be able to reverse blog category links, using a tempate tag
that is not in Wagtail core. In your project settings, add ``'wagtail.contrib.wagtailroutablepage'``
to ``INSTALLED_APPS``, then modify ``blog_index_page.html``:
.. code-block:: html+django
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% load wagtailroutablepage_tags %}
{% block body_class %}template-blogindexpage{% endblock %}
{% block content %}
{% if blogcats %}
<h3>Blog categories:</h3>
<ul>
{% for cat in blogcats %}
<li><a href="{% routablepageurl page "blog_category" cat.slug %}">
{{ cat.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
<h1>{{ page.title }}{% if category %} - {{ category.name }}{% endif %}</h1>
<div class="intro">{{ page.intro|richtext }}</div>
{% for post in blogpages %}
{% with post=post.specific %}
<h2><a href="{% slugurl post.slug %}">{{ post.title }}</a></h2>
{{ post.latest_revision_created_at }}<br />
{% if post.blog_categories.all %}
Filed under:
{% for cat in post.blog_categories.all %}
<a href="{% routablepageurl page "blog_category" cat.category.slug %}">
{{ cat.category.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}<br />
{% endif %}
{% if post.tags.all %}
Tags:
{% for tag in post.tags.all %}
<a href="{% slugurl 'tags' %}?tag={{ tag }}">
<button type="button">{{ tag }}</button></a>
{% endfor %}<br />
{% endif %}
<p>Intro: {{ post.intro }}</p>
{{ post.body|richtext }}
{% endwith %}
{% endfor %}
{% endblock %}
Study the "Filed under:" section -
we loop through each of a blog post's categories (if it has any), and for each, we reverse the URL
to the corresponding blog category view, using the URL we named earlier (``blog_category``), and
passing in the slug of the current category. We also display the category name in the header.
You'll probably want to do something similar on ``blog_page.html``.
And with that, we've got both tags and categories working, and our categories system is nice and dry.
.. figure:: ../_static/images/tutorial/tutorial_14.png
:alt: Blog category view
Where next
----------