Browse Source

Update Getting Started tutorial to include posts automatically, plus tagging
- Resolves #3096

Scot Hacker 8 years ago
parent
commit
32f97ff43a

BIN
docs/_static/images/tutorial/tutorial_4.png


BIN
docs/_static/images/tutorial/tutorial_4a.png


BIN
docs/_static/images/tutorial/tutorial_4b.png


BIN
docs/_static/images/tutorial/tutorial_7.png


BIN
docs/_static/images/tutorial/tutorial_8.png


BIN
docs/_static/images/tutorial/tutorial_9.png


+ 235 - 97
docs/getting_started/tutorial.rst

@@ -145,7 +145,56 @@ We are now ready to create a blog. To do so, run
 
 Add the new ``blog`` app to ``INSTALLED_APPS`` in ``mysite/settings/base.py``.
 
-The following example defines a basic blog post model in ``blog/models.py``:
+Blog Index and Posts
+~~~~~~~~~~~~~~~~~~~~
+
+Lets start with a simple index page for our blog. In ``blog/models.py``:
+
+.. code-block:: python
+
+    class BlogIndexPage(Page):
+        intro = RichTextField(blank=True)
+
+        content_panels = Page.content_panels + [
+            FieldPanel('intro', classname="full")
+        ]
+
+Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
+
+Since the model is called ``BlogIndexPage``, the default template name
+(unless we override it) will be ``blog/templates/blog/blog_index_page.html:``
+
+.. code-block:: html+django
+
+    {% extends "base.html" %}
+
+    {% load wagtailcore_tags %}
+
+    {% block body_class %}template-blogindexpage{% endblock %}
+
+    {% block content %}
+        <h1>{{ page.title }}</h1>
+
+        <div class="intro">{{ page.intro|richtext }}</div>
+
+        {% for post in page.get_children %}
+            <h2><a href="{% slugurl post.slug %}">{{ post.title }}</a></h2>
+            {{ post.specific.intro }}
+            {{ post.specific.body|richtext }}
+        {% endfor %}
+
+    {% endblock %}
+
+Most of this should be familiar, but we'll explain ``get_children`` a bit later.
+Note the ``slugurl`` tag, which is similar to Django's ``url`` tag but
+intended to take a Wagtail slug as an argument.
+
+In the Wagtail admin, create a ``BlogIndexPage`` under the Homepage,
+make sure it has the slug "blog" on the Promote tab, and publish it.
+You should now be able to access the url ``/blog`` on your site
+(note how the slug from the Promote tab defines the page URL).
+
+Now we need a model and template for our blog posts. In ``blog/models.py``:
 
 .. code-block:: python
 
@@ -173,7 +222,6 @@ The following example defines a basic blog post model in ``blog/models.py``:
             FieldPanel('body', classname="full")
         ]
 
-
 .. note::
    On Wagtail versions before 1.5, ``search_fields`` needs to be defined as a tuple:
 
@@ -185,6 +233,8 @@ The following example defines a basic blog post model in ``blog/models.py``:
         )
 
 
+Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
+
 Create a template at ``blog/templates/blog/blog_page.html``:
 
 .. code-block:: html+django
@@ -202,21 +252,85 @@ Create a template at ``blog/templates/blog/blog_page.html``:
         <div class="intro">{{ page.intro }}</div>
 
         {{ page.body|richtext }}
+
+        <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>
+
     {% endblock %}
 
-Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
+Note the use of Wagtail's built-in ``get_parent()`` method to obtain the
+URL of the blog this post is a part of.
+
+Now create a few blog posts as children of ``BlogIndexPage.``
+Be sure to select type "BlogPage" when creating your posts.
 
-.. figure:: ../_static/images/tutorial/tutorial_4.png
-   :alt: Create page screen
+.. figure:: ../_static/images/tutorial/tutorial_4a.png
+   :alt: Create blog post as child of BlogIndex
+
+.. figure:: ../_static/images/tutorial/tutorial_4b.png
+  :alt: Choose type BlogPost
+
+Wagtail gives you full control over what kinds of content can be created under
+various parent content types, but we won't go into that here. By default,
+any page type can be a child of any other page type.
 
 .. figure:: ../_static/images/tutorial/tutorial_5.png
    :alt: Page edit screen
 
+You should now have the very beginnings of a working blog.
+Access the ``/blog`` URL and you should see something like this:
+
+.. figure:: ../_static/images/tutorial/tutorial_7.png
+   :alt: Blog basics
+
+Titles should link to post pages, and a link back to the blog's
+homepage should appear in the footer of each post page.
+
+Parents and Children
+~~~~~~~~~~~~~~~~~~~~
+
+Much of the work you'll be doing in Wagtail revolves around the concept of hierarchical
+"tree" structures consisting of nodes and leaves (see :doc:`../reference/pages/theory`).
+In this case, the ``BlogIndexPage`` is a "node" and individual ``BlogPage`` instances
+are the "leaves".
+
+Take another look at the guts of ``BlogIndexPage:``
+
+.. code-block:: html+django
+
+    {% for post in page.get_children %}
+        <h2>{{ post.title }}</h2>
+        {{ post.specific.intro }}
+        {{ post.specific.body|richtext }}
+    {% endfor %}
+
+Every "page" in Wagtail can call out to its parent or children
+from its own position in the hierarchy. But why do we have to
+specify ``post.specific.title`` rather than ``post.title?``
+This has to do with the way we defined our model:
+
+``class BlogPage(Page):``
+
+The ``get_children()`` method got us a list of ``Page`` base classes. When we want to reference
+properties of the instances that inherits from the base class, Wagtail provides the ``specific``
+method that retrieves the actual ``BlogPage`` record.
+
+To tighten up template code like this, we could use Django's ``with`` tag:
+
+.. code-block:: html+django
+
+    {% for post in page.get_children %}
+        {% with post=post.specific %}
+            <h2>{{ post.title }}</h2>
+            {{ post.intro }}
+            {{ post.body|richtext }}
+        {% endwith %}
+    {% endfor %}
+
 Image support
 ~~~~~~~~~~~~~
 
-Wagtail provides support for images out of the box. To add them to your
-model:
+Wagtail provides support for images out of the box. To add them to
+your ``BlogPage`` model:
 
 .. code-block:: python
 
@@ -284,143 +398,167 @@ Adjust your blog page template to include the image:
 You can read more about using images in templates in the
 :doc:`docs <../topics/images>`.
 
-Blog Index
-~~~~~~~~~~
-
-Let us extend the Blog app to provide an index.
 
-.. code-block:: python
-
-    class BlogIndexPage(Page):
-        intro = RichTextField(blank=True)
-
-        content_panels = Page.content_panels + [
-            FieldPanel('intro', classname="full")
-        ]
-
-The above creates an index type to collect all our blog posts.
-
-``blog/templates/blog/blog_index_page.html``
-
-.. code-block:: html+django
-
-    {% extends "base.html" %}
-
-    {% load wagtailcore_tags %}
-
-    {% block body_class %}template-blogindexpage{% endblock %}
-
-    {% block content %}
-        <h1>{{ page.title }}</h1>
-
-        <div class="intro">{{ page.intro|richtext }}</div>
-    {% endblock %}
-
-Related items
+Tagging Posts
 ~~~~~~~~~~~~~
 
-Let's extend the BlogIndexPage to add related links. The related links
-can be BlogPages or external links. Change ``blog/models.py`` to
+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.
+
+First, alter ``models.py`` once more:
 
 .. code-block:: python
 
     from django.db import models
 
+    from modelcluster.tags import ClusterTaggableManager
     from modelcluster.fields import ParentalKey
+    from taggit.models import TaggedItemBase
 
-    from wagtail.wagtailcore.models import Page, Orderable
+    from wagtail.wagtailcore.models import Page
     from wagtail.wagtailcore.fields import RichTextField
-    from wagtail.wagtailadmin.edit_handlers import (FieldPanel,
-                                                    InlinePanel,
-                                                    MultiFieldPanel,
-                                                    PageChooserPanel)
+    from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel
     from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
     from wagtail.wagtailsearch import index
 
 
-    # ...
+    class BlogPageTag(TaggedItemBase):
+        content_object = ParentalKey('BlogPage', related_name='tagged_items')
 
-    class LinkFields(models.Model):
-        link_external = models.URLField("External link", blank=True)
-        link_page = models.ForeignKey(
-            'wagtailcore.Page',
+
+    class BlogPage(Page):
+        main_image = models.ForeignKey(
+            'wagtailimages.Image',
             null=True,
             blank=True,
+            on_delete=models.SET_NULL,
             related_name='+'
         )
+        date = models.DateField("Post date")
+        intro = models.CharField(max_length=250)
+        body = RichTextField(blank=True)
+        tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
 
-        @property
-        def link(self):
-            if self.link_page:
-                return self.link_page.url
-            else:
-                return self.link_external
+        search_fields = Page.search_fields + [
+            index.SearchField('intro'),
+            index.SearchField('body'),
+        ]
 
-        panels = [
-            FieldPanel('link_external'),
-            PageChooserPanel('link_page'),
+        content_panels = Page.content_panels + [
+            FieldPanel('date'),
+            ImageChooserPanel('main_image'),
+            FieldPanel('intro'),
+            FieldPanel('body'),
+            MultiFieldPanel([
+                FieldPanel('tags'),
+            ], heading="Tags"),
         ]
 
-        class Meta:
-            abstract = True
 
+    class BlogIndexPage(Page):
+        intro = RichTextField(blank=True)
 
-    # Related links
-    class RelatedLink(LinkFields):
-        title = models.CharField(max_length=255, help_text="Link title")
+Note the new ``modelcluster`` and ``taggit`` imports, the addition of a new
+``BlogPageTag`` model, the addition of a ``tags`` field on ``BlogPage``,
+and the use of ``MultiFieldPanel`` in ``content_panels`` to let users
+select tags.
 
-        panels = [
-            FieldPanel('title'),
-            MultiFieldPanel(LinkFields.panels, "Link"),
-        ]
+Edit one of your ``BlogPage`` instances, and you should now be able to tag posts:
 
-        class Meta:
-            abstract = True
+.. figure:: ../_static/images/tutorial/tutorial_8.png
+   :alt: Tagging a post
 
+To render tags on a ``BlogPage``, add this to ``blog_page.html:``
 
-    class BlogIndexPage(Page):
-        intro = RichTextField(blank=True)
+.. code-block:: html+django
 
-        content_panels = Page.content_panels + [
-            FieldPanel('intro', classname="full"),
-            InlinePanel('related_links', label="Related links"),
-        ]
+    {% if page.specific.tags.all.count %}
+        <div class="tags">
+            <h3>Tags</h3>
+            {% for tag in page.specific.tags.all %}
+                <a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
+            {% endfor %}
+        </div>
+    {% endif %}
+
+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
+will get you a 404, since we haven't yet defined a "tags" view, which
+is going to require a little extra magic. Add to ``models.py:``
 
+.. code-block:: python
 
-    class BlogIndexRelatedLink(Orderable, RelatedLink):
-        page = ParentalKey('BlogIndexPage', related_name='related_links')
+    class BlogTagIndexPage(Page):
 
-.. figure:: ../_static/images/tutorial/tutorial_7.png
-   :alt: Blog index edit screen
+        def get_context(self, request):
+
+            # Filter by tag
+            tag = request.GET.get('tag')
+            blogpages = BlogPage.objects.filter().filter(tags__name=tag)
+
+            # Update template context
+            context = super(BlogTagIndexPage, self).get_context(request)
+            context['blogpages'] = blogpages
+            return context
 
-Extend ``blog_index_page.html`` to show related items
+Note that this Page-based model defines no fields of its own.
+Even without fields, subclassing ``Page`` makes it a part of the
+Wagtail ecosystem, so that you can give it a title and URL in the
+admin, and so that you can manipulate its contents by returning
+a queryset from its ``get_context()`` method.
+
+Migrate this in, then create a new ``BlogTagIndexPage`` in the admin.
+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.
+
+Access ``/tags`` and Django will tell you what you probably already knew:
+you need to create a template ``blog/blog_tag_index_page.html:``
 
 .. code-block:: html+django
 
     {% extends "base.html" %}
-
     {% load wagtailcore_tags %}
 
-    {% block body_class %}template-blogindexpage{% endblock %}
-
     {% block content %}
-        <h1>{{ page.title }}</h1>
-
-        <div class="intro">{{ page.intro|richtext }}</div>
 
-        {% if page.related_links.all %}
-            <ul>
-                {% for item in page.related_links.all %}
-                    <li><a href="{{ item.link }}">{{ item.title }}</a></li>
-                {% endfor %}
-            </ul>
+        {% if request.GET.tag|length %}
+            <h4>Showing pages tagged "{{ request.GET.tag|safe }}"</h4>
         {% endif %}
+
+        {% for blogpage in blogpages %}
+
+              <p>
+                  <strong><a href="{% pageurl blogpage %}">{{ blogpage.title }}</a></strong><br />
+                  <small>Revised: {{ blogpage.latest_revision_created_at }}</small><br />
+                  {% if blogpage.author %}
+                    <p>By {{ blogpage.author.profile }}</p>
+                  {% endif %}
+              </p>
+
+        {% empty %}
+            No pages found with that tag.
+        {% endfor %}
+
     {% endblock %}
 
-You now have a fully working blog with featured blog posts.
+Unlike in the previous example, we're linking to pages here with the builtin ``pageurl``
+tag rather than ``slugurl``. The difference is that ``pageurl`` takes a Page object
+as an argument. Use whichever one best suits your purpose.
 
-.. figure:: ../_static/images/tutorial/tutorial_8.png
-   :alt: Barebones blog index
+We're also calling the built-in ``latest_revision_created_at`` field on the ``Page``
+model - handy to know this is always available.
+
+We haven't yet added an "author" field to our ``BlogPage`` model, nor do we have
+a Profile model for authors  - we'll leave those as an exercise for the reader.
+
+Clicking the tag button at the bottom of a BlogPost should now render a page
+something like this:
+
+.. figure:: ../_static/images/tutorial/tutorial_9.png
+   :alt: A simple tag view
 
 Where next
 ----------