kopia lustrzana https://github.com/wagtail/wagtail
Remove categories section, add better example of MultiFieldPanel, minor copyediting
rodzic
756d5ec0dd
commit
011f2099eb
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 222 KiB Po Szerokość: | Wysokość: | Rozmiar: 138 KiB |
|
@ -152,6 +152,10 @@ Lets start with a simple index page for our blog. In ``blog/models.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
from wagtail.wagtailcore.models import Page
|
||||||
|
from wagtail.wagtailcore.fields import RichTextField
|
||||||
|
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
||||||
|
|
||||||
class BlogIndexPage(Page):
|
class BlogIndexPage(Page):
|
||||||
intro = RichTextField(blank=True)
|
intro = RichTextField(blank=True)
|
||||||
|
|
||||||
|
@ -206,6 +210,9 @@ Now we need a model and template for our blog posts. In ``blog/models.py``:
|
||||||
from wagtail.wagtailsearch import index
|
from wagtail.wagtailsearch import index
|
||||||
|
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
|
||||||
class BlogPage(Page):
|
class BlogPage(Page):
|
||||||
date = models.DateField("Post date")
|
date = models.DateField("Post date")
|
||||||
intro = models.CharField(max_length=250)
|
intro = models.CharField(max_length=250)
|
||||||
|
@ -287,7 +294,7 @@ Take another look at the guts of ``BlogIndexPage:``
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
{% for post in page.get_children %}
|
{% for post in page.get_children %}
|
||||||
<h2>{{ post.title }}</h2>
|
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
|
||||||
{{ post.specific.intro }}
|
{{ post.specific.intro }}
|
||||||
{{ post.specific.body|richtext }}
|
{{ post.specific.body|richtext }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -311,7 +318,7 @@ To tighten up template code like this, we could use Django's ``with`` tag:
|
||||||
|
|
||||||
{% for post in page.get_children %}
|
{% for post in page.get_children %}
|
||||||
{% with post=post.specific %}
|
{% with post=post.specific %}
|
||||||
<h2>{{ post.title }}</h2>
|
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
|
||||||
<p>{{ post.intro }}</p>
|
<p>{{ post.intro }}</p>
|
||||||
{{ post.body|richtext }}
|
{{ post.body|richtext }}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -362,14 +369,14 @@ model like this:
|
||||||
context['blogpages'] = blogpages
|
context['blogpages'] = blogpages
|
||||||
return context
|
return context
|
||||||
|
|
||||||
All we've done here is to retrieve the original context, create a custom queryset,
|
All we've done here is retrieve the original context, create a custom queryset,
|
||||||
add it to the retrieved context, and return the modified context back to the view.
|
add it to the retrieved context, and return the modified context back to the view.
|
||||||
You'll also need to modify your ``blog_index_page.html`` template slightly.
|
You'll also need to modify your ``blog_index_page.html`` template slightly.
|
||||||
Change:
|
Change:
|
||||||
|
|
||||||
``{% for post in page.get_children %} to {% for post in blogpages %}``
|
``{% for post in page.get_children %}`` to ``{% for post in blogpages %}``
|
||||||
|
|
||||||
Now try Unpublishing one of your posts - it should disappear from the blog index
|
Now try unpublishing one of your posts - it should disappear from the blog index
|
||||||
page. The remaining posts should now be sorted with the most recently modified
|
page. The remaining posts should now be sorted with the most recently modified
|
||||||
posts first.
|
posts first.
|
||||||
|
|
||||||
|
@ -446,17 +453,14 @@ You can read more about using images in templates in the
|
||||||
:doc:`docs <../topics/images>`.
|
:doc:`docs <../topics/images>`.
|
||||||
|
|
||||||
|
|
||||||
Tags and Categories
|
Tagging Posts
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
What's a blog without a taxonomy? You'll probably want Categories
|
Let's say we want to let editors "tag" their posts, so that readers can, e.g.,
|
||||||
for "big picture" organization ("News," "Sports," "Politics," etc.) and Tags
|
view all bicycle-related content together. For this, we'll need to invoke
|
||||||
for fine-grained sorting ("Bicycle," "Clinton," "Electric Vehicles," etc.)
|
the tagging system bundled with Wagtail, attach it to the ``BlogPage``
|
||||||
You'll need mechanisms to let editors manage tags categories and attach them to posts,
|
model and content panels, and render linked tags on the blog post template.
|
||||||
ways to display them on your blog pages, and views that display all posts belonging
|
Of course, we'll need a working tag-specific URL view as well.
|
||||||
to a given tag or category.
|
|
||||||
|
|
||||||
Let's start with tags, since they're bundled with Wagtail.
|
|
||||||
|
|
||||||
First, alter ``models.py`` once more:
|
First, alter ``models.py`` once more:
|
||||||
|
|
||||||
|
@ -498,13 +502,13 @@ First, alter ``models.py`` once more:
|
||||||
]
|
]
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
|
MultiFieldPanel([
|
||||||
FieldPanel('date'),
|
FieldPanel('date'),
|
||||||
|
FieldPanel('tags'),
|
||||||
|
], heading="Blog information"),
|
||||||
ImageChooserPanel('main_image'),
|
ImageChooserPanel('main_image'),
|
||||||
FieldPanel('intro'),
|
FieldPanel('intro'),
|
||||||
FieldPanel('body'),
|
FieldPanel('body'),
|
||||||
MultiFieldPanel([
|
|
||||||
FieldPanel('tags'),
|
|
||||||
], heading="Tags"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -512,23 +516,23 @@ First, alter ``models.py`` once more:
|
||||||
intro = RichTextField(blank=True)
|
intro = RichTextField(blank=True)
|
||||||
|
|
||||||
Note the new ``modelcluster`` and ``taggit`` imports, the addition of a new
|
Note the new ``modelcluster`` and ``taggit`` imports, the addition of a new
|
||||||
``BlogPageTag`` model, the addition of a ``tags`` field on ``BlogPage``,
|
``BlogPageTag`` model, and the addition of a ``tags`` field on ``BlogPage``.
|
||||||
and the use of ``MultiFieldPanel`` in ``content_panels`` to let users
|
We've also taken the opportunity to use a ``MultiFieldPanel`` in ``content_panels``
|
||||||
select tags.
|
to group the date and tags fields together for readability.
|
||||||
|
|
||||||
Edit one of your ``BlogPage`` instances, and you should now be able to tag posts:
|
Edit one of your ``BlogPage`` instances, and you should now be able to tag posts:
|
||||||
|
|
||||||
.. figure:: ../_static/images/tutorial/tutorial_8.png
|
.. figure:: ../_static/images/tutorial/tutorial_8.png
|
||||||
:alt: Tagging a post
|
:alt: Tagging a post
|
||||||
|
|
||||||
To render tags on a ``BlogPage,`` add this to ``blog_page.html:``
|
To render tags on a ``BlogPage``, add this to ``blog_page.html``:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
{% if page.specific.tags.all.count %}
|
{% if page.tags.all.count %}
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<h3>Tags</h3>
|
<h3>Tags</h3>
|
||||||
{% for tag in page.specific.tags.all %}
|
{% for tag in page.tags.all %}
|
||||||
<a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
|
<a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -542,7 +546,7 @@ isn't readily available, so we fall back on the less-preferred ``slugurl`` tag.
|
||||||
|
|
||||||
Visiting a blog post with tags should now show a set of linked
|
Visiting a blog post with tags should now show a set of linked
|
||||||
buttons at the bottom - one for each tag. However, clicking a button
|
buttons at the bottom - one for each tag. However, clicking a button
|
||||||
will get you a 404, since we haven't yet defined a "tags" view. Add to ``models.py:``
|
will get you a 404, since we haven't yet defined a "tags" view. Add to ``models.py``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -570,7 +574,7 @@ You'll probably want to create the new page/view under Homepage,
|
||||||
parallel to your Blog index. Give it the slug "tags" on the Promote tab.
|
parallel to your Blog index. Give it the slug "tags" on the Promote tab.
|
||||||
|
|
||||||
Access ``/tags`` and Django will tell you what you probably already knew:
|
Access ``/tags`` and Django will tell you what you probably already knew:
|
||||||
you need to create a template ``blog/blog_tag_index_page.html:``
|
you need to create a template ``blog/blog_tag_index_page.html``:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
@ -611,237 +615,6 @@ something like this:
|
||||||
.. figure:: ../_static/images/tutorial/tutorial_9.png
|
.. figure:: ../_static/images/tutorial/tutorial_9.png
|
||||||
:alt: A simple tag view
|
: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 in
|
|
||||||
Wagtail's "contrib" module, not in 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="{% pageurl post %}">{{ 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
|
|
||||||
because we used a single model (view) to handle both the blog's homepage and its category displays.
|
|
||||||
|
|
||||||
.. figure:: ../_static/images/tutorial/tutorial_14.png
|
|
||||||
:alt: Blog category view
|
|
||||||
|
|
||||||
|
|
||||||
Where next
|
Where next
|
||||||
----------
|
----------
|
||||||
|
|
Ładowanie…
Reference in New Issue