tutorial.rst 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  1. Your first Wagtail site
  2. =======================
  3. .. note::
  4. This tutorial covers setting up a brand new Wagtail project. If you'd like to add Wagtail to an existing Django project instead, see :doc:`integrating_into_django`.
  5. 1. Install Wagtail and its dependencies::
  6. pip install wagtail
  7. 2. Start your site::
  8. wagtail start mysite
  9. cd mysite
  10. Wagtail provides a ``start`` command similar to
  11. ``django-admin.py startproject``. Running ``wagtail start mysite`` in
  12. your project will generate a new ``mysite`` folder with a few
  13. Wagtail-specific extras, including the required project settings, a
  14. "home" app with a blank ``HomePage`` model and basic templates and a sample
  15. "search" app.
  16. 3. Install project dependencies::
  17. pip install -r requirements.txt
  18. This ensures that you have the relevant version of Django for the project you've just created.
  19. 4. Create the database::
  20. python manage.py migrate
  21. If you haven't updated the project settings, this will be a SQLite
  22. database file in the project directory.
  23. 5. Create an admin user::
  24. python manage.py createsuperuser
  25. 6. ``python manage.py runserver`` If everything worked,
  26. http://127.0.0.1:8000 will show you a welcome page
  27. .. figure:: ../_static/images/tutorial/tutorial_1.png
  28. :alt: Wagtail welcome message
  29. You can now access the administrative area at http://127.0.0.1:8000/admin
  30. .. figure:: ../_static/images/tutorial/tutorial_2.png
  31. :alt: Administrative screen
  32. Extend the HomePage model
  33. -------------------------
  34. Out of the box, the "home" app defines a blank ``HomePage`` model in ``models.py``, along with a migration that creates a homepage and configures Wagtail to use it.
  35. Edit ``home/models.py`` as follows, to add a ``body`` field to the model:
  36. .. code-block:: python
  37. from __future__ import unicode_literals
  38. from django.db import models
  39. from wagtail.wagtailcore.models import Page
  40. from wagtail.wagtailcore.fields import RichTextField
  41. from wagtail.wagtailadmin.edit_handlers import FieldPanel
  42. class HomePage(Page):
  43. body = RichTextField(blank=True)
  44. content_panels = Page.content_panels + [
  45. FieldPanel('body', classname="full")
  46. ]
  47. ``body`` is defined as ``RichTextField``, a special Wagtail field. You
  48. can use any of the `Django core fields <https://docs.djangoproject.com/en/1.8/ref/models/fields/>`__. ``content_panels`` define the
  49. capabilities and the layout of the editing interface. :doc:`More on creating Page models. <../topics/pages>`
  50. Run ``python manage.py makemigrations``, then
  51. ``python manage.py migrate`` to update the database with your model
  52. changes. You must run the above commands each time you make changes to
  53. the model definition.
  54. You can now edit the homepage within the Wagtail admin area (go to Explorer, Homepage, then Edit) to see the new body field. Enter some text into the body field, and publish the page.
  55. The page template now needs to be updated to reflect the changes made
  56. to the model. Wagtail uses normal Django templates to render each page
  57. type. It automatically generates a template filename from the model name
  58. by separating capital letters with underscores (e.g. HomePage becomes
  59. home\_page.html). Edit
  60. ``home/templates/home/home_page.html`` to contain the following:
  61. .. code-block:: html+django
  62. {% extends "base.html" %}
  63. {% load wagtailcore_tags %}
  64. {% block body_class %}template-homepage{% endblock %}
  65. {% block content %}
  66. {{ page.body|richtext }}
  67. {% endblock %}
  68. .. figure:: ../_static/images/tutorial/tutorial_3.png
  69. :alt: Updated homepage
  70. Wagtail template tags
  71. ~~~~~~~~~~~~~~~~~~~~~
  72. Wagtail provides a number of :ref:`template tags & filters <template-tags-and-filters>`
  73. which can be loaded by including ``{% load wagtailcore_tags %}`` at the top of
  74. your template file.
  75. In this tutorial, we use the `richtext` filter to escape and print the contents
  76. of a ``RichTextField``:
  77. .. code-block:: html+django
  78. {% load wagtailcore_tags %}
  79. {{ page.body|richtext }}
  80. Produces:
  81. .. code-block:: html
  82. <div class="rich-text">
  83. <p>
  84. <b>Welcome</b> to our new site!
  85. </p>
  86. </div>
  87. **Note:** You'll need to include ``{% load wagtailcore_tags %}`` in each
  88. template that uses Wagtail's tags. Django will throw a ``TemplateSyntaxError``
  89. if the tags aren't loaded.
  90. A basic blog
  91. ------------
  92. We are now ready to create a blog. To do so, run
  93. ``python manage.py startapp blog`` to create a new app in your Wagtail site.
  94. Add the new ``blog`` app to ``INSTALLED_APPS`` in ``mysite/settings/base.py``.
  95. Blog Index and Posts
  96. ~~~~~~~~~~~~~~~~~~~~
  97. Lets start with a simple index page for our blog. In ``blog/models.py``:
  98. .. code-block:: python
  99. class BlogIndexPage(Page):
  100. intro = RichTextField(blank=True)
  101. content_panels = Page.content_panels + [
  102. FieldPanel('intro', classname="full")
  103. ]
  104. Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
  105. Since the model is called ``BlogIndexPage``, the default template name
  106. (unless we override it) will be ``blog/templates/blog/blog_index_page.html:``
  107. .. code-block:: html+django
  108. {% extends "base.html" %}
  109. {% load wagtailcore_tags %}
  110. {% block body_class %}template-blogindexpage{% endblock %}
  111. {% block content %}
  112. <h1>{{ page.title }}</h1>
  113. <div class="intro">{{ page.intro|richtext }}</div>
  114. {% for post in page.get_children %}
  115. <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
  116. {{ post.specific.intro }}
  117. {{ post.specific.body|richtext }}
  118. {% endfor %}
  119. {% endblock %}
  120. Most of this should be familiar, but we'll explain ``get_children`` a bit later.
  121. Note the ``pageurl`` tag, which is similar to Django's ``url`` tag but
  122. takes a Wagtail Page object as an argument.
  123. In the Wagtail admin, create a ``BlogIndexPage`` under the Homepage,
  124. make sure it has the slug "blog" on the Promote tab, and publish it.
  125. You should now be able to access the url ``/blog`` on your site
  126. (note how the slug from the Promote tab defines the page URL).
  127. Now we need a model and template for our blog posts. In ``blog/models.py``:
  128. .. code-block:: python
  129. from django.db import models
  130. from wagtail.wagtailcore.models import Page
  131. from wagtail.wagtailcore.fields import RichTextField
  132. from wagtail.wagtailadmin.edit_handlers import FieldPanel
  133. from wagtail.wagtailsearch import index
  134. class BlogPage(Page):
  135. date = models.DateField("Post date")
  136. intro = models.CharField(max_length=250)
  137. body = RichTextField(blank=True)
  138. search_fields = Page.search_fields + [
  139. index.SearchField('intro'),
  140. index.SearchField('body'),
  141. ]
  142. content_panels = Page.content_panels + [
  143. FieldPanel('date'),
  144. FieldPanel('intro'),
  145. FieldPanel('body', classname="full")
  146. ]
  147. Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
  148. Create a template at ``blog/templates/blog/blog_page.html``:
  149. .. code-block:: html+django
  150. {% extends "base.html" %}
  151. {% load wagtailcore_tags %}
  152. {% block body_class %}template-blogpage{% endblock %}
  153. {% block content %}
  154. <h1>{{ page.title }}</h1>
  155. <p class="meta">{{ page.date }}</p>
  156. <div class="intro">{{ page.intro }}</div>
  157. {{ page.body|richtext }}
  158. <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>
  159. {% endblock %}
  160. Note the use of Wagtail's built-in ``get_parent()`` method to obtain the
  161. URL of the blog this post is a part of.
  162. Now create a few blog posts as children of ``BlogIndexPage.``
  163. Be sure to select type "BlogPage" when creating your posts.
  164. .. figure:: ../_static/images/tutorial/tutorial_4a.png
  165. :alt: Create blog post as child of BlogIndex
  166. .. figure:: ../_static/images/tutorial/tutorial_4b.png
  167. :alt: Choose type BlogPost
  168. Wagtail gives you full control over what kinds of content can be created under
  169. various parent content types. By default, any page type can be a child of any
  170. other page type.
  171. .. figure:: ../_static/images/tutorial/tutorial_5.png
  172. :alt: Page edit screen
  173. You should now have the very beginnings of a working blog.
  174. Access the ``/blog`` URL and you should see something like this:
  175. .. figure:: ../_static/images/tutorial/tutorial_7.png
  176. :alt: Blog basics
  177. Titles should link to post pages, and a link back to the blog's
  178. homepage should appear in the footer of each post page.
  179. Parents and Children
  180. ~~~~~~~~~~~~~~~~~~~~
  181. Much of the work you'll be doing in Wagtail revolves around the concept of hierarchical
  182. "tree" structures consisting of nodes and leaves (see :doc:`../reference/pages/theory`).
  183. In this case, the ``BlogIndexPage`` is a "node" and individual ``BlogPage`` instances
  184. are the "leaves".
  185. Take another look at the guts of ``BlogIndexPage:``
  186. .. code-block:: html+django
  187. {% for post in page.get_children %}
  188. <h2>{{ post.title }}</h2>
  189. {{ post.specific.intro }}
  190. {{ post.specific.body|richtext }}
  191. {% endfor %}
  192. Every "page" in Wagtail can call out to its parent or children
  193. from its own position in the hierarchy. But why do we have to
  194. specify ``post.specific.intro`` rather than ``post.intro?``
  195. This has to do with the way we defined our model:
  196. ``class BlogPage(Page):``
  197. The ``get_children()`` method gets us a list of ``Page`` base classes. When we want to reference
  198. properties of the instances that inherit from the base class, Wagtail provides the ``specific``
  199. method that retrieves the actual ``BlogPage`` record. While the "title" field is present on
  200. the base ``Page`` model, "intro" is only present on the ``BlogPage`` model, so we need
  201. ``.specific`` to access it.
  202. To tighten up template code like this, we could use Django's ``with`` tag:
  203. .. code-block:: html+django
  204. {% for post in page.get_children %}
  205. {% with post=post.specific %}
  206. <h2>{{ post.title }}</h2>
  207. <p>{{ post.intro }}</p>
  208. {{ post.body|richtext }}
  209. {% endwith %}
  210. {% endfor %}
  211. When you start writing more customized Wagtail code, you'll find a whole set of QuerySet
  212. modifiers to help you navigate the hierarchy.
  213. .. code-block:: python
  214. # Given a page object 'somepage':
  215. MyModel.objects.descendant_of(somepage)
  216. child_of(page) / not_child_of(somepage)
  217. ancestor_of(somepage) / not_ancestor_of(somepage)
  218. parent_of(somepage) / not_parent_of(somepage)
  219. sibling_of(somepage) / not_sibling_of(somepage)
  220. # ... and ...
  221. somepage.get_children()
  222. somepage.get_ancestors()
  223. somepage.get_descendants()
  224. somepage.get_siblings()
  225. For more information, see: :doc:`../reference/pages/queryset_reference`
  226. Overriding Context
  227. ~~~~~~~~~~~~~~~~~~
  228. There are a couple of problems with our blog index view:
  229. 1) Blogs generally display content in reverse chronological order
  230. 2) We want to make sure we're only displaying *published* content.
  231. To accomplish these things, we need to do more than just grab the index
  232. page's children in the template. Instead, we'll want to modify the
  233. QuerySet in the model definition. Wagtail makes this possible via
  234. the overridable ``get_context()`` method. Modify your ``BlogIndexPage``
  235. model like this:
  236. .. code-block:: python
  237. class BlogIndexPage(Page):
  238. intro = RichTextField(blank=True)
  239. def get_context(self, request):
  240. # Update context to include only published posts, ordered by reverse-chron
  241. context = super(BlogIndexPage, self).get_context(request)
  242. blogpages = self.get_children().live().order_by('-first_published_at')
  243. context['blogpages'] = blogpages
  244. return context
  245. You'll also need to modify your ``blog_index_page.html`` template slightly.
  246. Change:
  247. ``{% for post in page.get_children %} to {% for post in blogpages %}``
  248. Now try Unpublishing one of your posts - it should disappear from the blog index
  249. page. The remaining posts should now be sorted with the most recently modified
  250. posts first.
  251. Image support
  252. ~~~~~~~~~~~~~
  253. Wagtail provides support for images out of the box. To add them to
  254. your ``BlogPage`` model:
  255. .. code-block:: python
  256. from django.db import models
  257. from wagtail.wagtailcore.models import Page
  258. from wagtail.wagtailcore.fields import RichTextField
  259. from wagtail.wagtailadmin.edit_handlers import FieldPanel
  260. from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
  261. from wagtail.wagtailsearch import index
  262. class BlogPage(Page):
  263. main_image = models.ForeignKey(
  264. 'wagtailimages.Image',
  265. null=True,
  266. blank=True,
  267. on_delete=models.SET_NULL,
  268. related_name='+'
  269. )
  270. date = models.DateField("Post date")
  271. intro = models.CharField(max_length=250)
  272. body = RichTextField(blank=True)
  273. search_fields = Page.search_fields + [
  274. index.SearchField('intro'),
  275. index.SearchField('body'),
  276. ]
  277. content_panels = Page.content_panels + [
  278. FieldPanel('date'),
  279. ImageChooserPanel('main_image'),
  280. FieldPanel('intro'),
  281. FieldPanel('body'),
  282. ]
  283. Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
  284. Adjust your blog page template to include the image:
  285. .. code-block:: html+django
  286. {% extends "base.html" %}
  287. {% load wagtailcore_tags wagtailimages_tags %}
  288. {% block body_class %}template-blogpage{% endblock %}
  289. {% block content %}
  290. <h1>{{ page.title }}</h1>
  291. <p class="meta">{{ page.date }}</p>
  292. {% if page.main_image %}
  293. {% image page.main_image width-400 %}
  294. {% endif %}
  295. <div class="intro">{{ page.intro }}</div>
  296. {{ page.body|richtext }}
  297. {% endblock %}
  298. .. figure:: ../_static/images/tutorial/tutorial_6.png
  299. :alt: A blog post sample
  300. You can read more about using images in templates in the
  301. :doc:`docs <../topics/images>`.
  302. Tags and Categories
  303. ~~~~~~~~~~~~~~~~~~~
  304. What's a blog without a solid taxonomy? You'll probably want Categories
  305. for "big picture" taxonomy ("News," "Sports," "Politics," etc.) and Tags
  306. for fine-grained sorting ("Bicycle," "Clinton," "Electric Vehicles," etc.)
  307. You'll need mechanisms to let editors manage tags categories and attach them to posts,
  308. ways to display them on your blog pages, and views that display all posts belonging
  309. to a given tag or category.
  310. Let's start with tags, since they're bundled with Wagtail.
  311. First, alter ``models.py`` once more:
  312. .. code-block:: python
  313. from django.db import models
  314. from modelcluster.tags import ClusterTaggableManager
  315. from modelcluster.fields import ParentalKey
  316. from taggit.models import TaggedItemBase
  317. from wagtail.wagtailcore.models import Page
  318. from wagtail.wagtailcore.fields import RichTextField
  319. from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel
  320. from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
  321. from wagtail.wagtailsearch import index
  322. class BlogPageTag(TaggedItemBase):
  323. content_object = ParentalKey('BlogPage', related_name='tagged_items')
  324. class BlogPage(Page):
  325. main_image = models.ForeignKey(
  326. 'wagtailimages.Image',
  327. null=True,
  328. blank=True,
  329. on_delete=models.SET_NULL,
  330. related_name='+'
  331. )
  332. date = models.DateField("Post date")
  333. intro = models.CharField(max_length=250)
  334. body = RichTextField(blank=True)
  335. tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
  336. search_fields = Page.search_fields + [
  337. index.SearchField('intro'),
  338. index.SearchField('body'),
  339. ]
  340. content_panels = Page.content_panels + [
  341. FieldPanel('date'),
  342. ImageChooserPanel('main_image'),
  343. FieldPanel('intro'),
  344. FieldPanel('body'),
  345. MultiFieldPanel([
  346. FieldPanel('tags'),
  347. ], heading="Tags"),
  348. ]
  349. class BlogIndexPage(Page):
  350. intro = RichTextField(blank=True)
  351. Note the new ``modelcluster`` and ``taggit`` imports, the addition of a new
  352. ``BlogPageTag`` model, the addition of a ``tags`` field on ``BlogPage``,
  353. and the use of ``MultiFieldPanel`` in ``content_panels`` to let users
  354. select tags.
  355. Edit one of your ``BlogPage`` instances, and you should now be able to tag posts:
  356. .. figure:: ../_static/images/tutorial/tutorial_8.png
  357. :alt: Tagging a post
  358. To render tags on a ``BlogPage,`` add this to ``blog_page.html:``
  359. .. code-block:: html+django
  360. {% if page.specific.tags.all.count %}
  361. <div class="tags">
  362. <h3>Tags</h3>
  363. {% for tag in page.specific.tags.all %}
  364. <a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
  365. {% endfor %}
  366. </div>
  367. {% endif %}
  368. Visiting a blog post with tags should now show a set of linked
  369. buttons at the bottom - one for each tag. However, clicking a button
  370. will get you a 404, since we haven't yet defined a "tags" view, which
  371. is going to require a little extra magic. Add to ``models.py:``
  372. .. code-block:: python
  373. class BlogTagIndexPage(Page):
  374. def get_context(self, request):
  375. # Filter by tag
  376. tag = request.GET.get('tag')
  377. blogpages = BlogPage.objects.filter().filter(tags__name=tag)
  378. # Update template context
  379. context = super(BlogTagIndexPage, self).get_context(request)
  380. context['blogpages'] = blogpages
  381. return context
  382. Note that this Page-based model defines no fields of its own.
  383. Even without fields, subclassing ``Page`` makes it a part of the
  384. Wagtail ecosystem, so that you can give it a title and URL in the
  385. admin, and so that you can manipulate its contents by returning
  386. a queryset from its ``get_context()`` method.
  387. Migrate this in, then create a new ``BlogTagIndexPage`` in the admin.
  388. You'll probably want to create the new page/view under Homepage,
  389. parallel to your Blog index. Give it the slug "tags" on the Promote tab.
  390. Access ``/tags`` and Django will tell you what you probably already knew:
  391. you need to create a template ``blog/blog_tag_index_page.html:``
  392. .. code-block:: html+django
  393. {% extends "base.html" %}
  394. {% load wagtailcore_tags %}
  395. {% block content %}
  396. {% if request.GET.tag|length %}
  397. <h4>Showing pages tagged "{{ request.GET.tag }}"</h4>
  398. {% endif %}
  399. {% for blogpage in blogpages %}
  400. <p>
  401. <strong><a href="{% slugurl blogpage.slug %}">{{ blogpage.title }}</a></strong><br />
  402. <small>Revised: {{ blogpage.latest_revision_created_at }}</small><br />
  403. {% if blogpage.author %}
  404. <p>By {{ blogpage.author.profile }}</p>
  405. {% endif %}
  406. </p>
  407. {% empty %}
  408. No pages found with that tag.
  409. {% endfor %}
  410. {% endblock %}
  411. Unlike in the previous example, we're linking to pages here with the builtin ``slugurl``
  412. tag rather than ``pageurl``. The difference is that ``slugurl`` takes a Page slug
  413. (from the Promote tab) as an argument. ``pageurl`` is more commonly used because it
  414. is unambiguous, but use whichever one best suits your purpose.
  415. We're also calling the built-in ``latest_revision_created_at`` field on the ``Page``
  416. model - handy to know this is always available.
  417. We haven't yet added an "author" field to our ``BlogPage`` model, nor do we have
  418. a Profile model for authors - we'll leave those as an exercise for the reader.
  419. Clicking the tag button at the bottom of a BlogPost should now render a page
  420. something like this:
  421. .. figure:: ../_static/images/tutorial/tutorial_9.png
  422. :alt: A simple tag view
  423. Categories
  424. ~~~~~~~~~~
  425. Now to add a Categories system. Again, alter ``models.py``:
  426. .. code-block:: python
  427. class BlogCategory(models.Model):
  428. name = models.CharField(max_length=256)
  429. slug = models.CharField(max_length=12)
  430. def __str__(self):
  431. return self.name
  432. class Meta:
  433. verbose_name_plural = "Blog Categories"
  434. This model does *not* subclass the Wagtail ``Page``
  435. model, and is *not* a Wagtail Snippet - it's a standard Django model! While we could have created
  436. categories as Pages, that wouldn't really make a lot of sense - while we'll eventually
  437. want pages for our categories, a category itself is more of a metadata storage structure than a page,
  438. so it makes sense to make it a vanilla Django model. As a result, this exercise will also show
  439. how to integrate non-Wagtail models into the Wagtail workflow.
  440. As an aside, the ``BlogCategory`` model could easily live in a totally different app of your
  441. Django project, and just be imported normally into your Wagtail blog app. This would be important if you were, e.g.,
  442. integrating a Wagtail blog into a pre-existing Django site that already had a system of categories.
  443. We want to create a ManyToMany relationship between BlogCategory and BlogPage. In standard Django, we would do
  444. something like this:
  445. ``categories = models.ManyToManyField(BlogCategory, blank=True)``
  446. However, it's a bit trickier than that with Wagtail because of the ``modelcluster`` dependency it
  447. uses to maintain hierarchical relationships. ``modelcluster`` is at the heart of Wagtail, but does not
  448. support M2M relationships. Instead, we'll need to define the related table manually:
  449. .. code-block:: python
  450. class BlogCategoryBlogPage(Orderable, models.Model):
  451. category = models.ForeignKey(BlogCategory, related_name="+")
  452. page = ParentalKey(BlogPage, related_name='blog_categories')
  453. This model's table will store relationships between blog pages and the categories assigned to them,
  454. effectively giving us the equivalent of a ManyToMany relationship. For readability, we named the class
  455. by concatenating the names of the two related models. The class also subclasses ``Orderable``,
  456. which means you'll be able to control the order of Categories on a blog post via the Wagtail admin.
  457. Now we just need to attach a "panel" for the relationship to our BlogPost. In the ``BlogPost`` model,
  458. add an ``InlinePanel`` for the "related_name" ``blog_categories:``
  459. .. code-block:: python
  460. content_panels = Page.content_panels + [
  461. FieldPanel('date'),
  462. ImageChooserPanel('main_image'),
  463. FieldPanel('intro'),
  464. FieldPanel('body'),
  465. InlinePanel('blog_categories', label="Blog Categories"),
  466. MultiFieldPanel([
  467. FieldPanel('tags'),
  468. ], heading="Tags"),
  469. ]
  470. Run ``python manage.py makemigrations`` and ``python manage.py migrate,`` then view an admin page for a ``BlogPage:``
  471. .. figure:: ../_static/images/tutorial/tutorial_10.png
  472. :alt: A category picker for BlogPage
  473. At first, we have no categories to choose from. Unlike the Django admin, we can't add them on the fly from here.
  474. Since we didn't create ``BlogCategory`` as a Page or Snippet, Wagtail isn't automatically aware of it, so we'll
  475. need to expose it in the admin manually. Fortunately, Wagtail provides a mechanism for this,
  476. via ``ModelAdmin``. Create a new file in your blog app, called ``wagtail_hooks.py:``
  477. .. code-block:: python
  478. from wagtail.contrib.modeladmin.options import (ModelAdmin, modeladmin_register)
  479. from blog.models import BlogCategory
  480. class BlogCategoryAdmin(ModelAdmin):
  481. model = BlogCategory
  482. add_to_settings_menu = True
  483. list_display = ('name', 'slug')
  484. modeladmin_register(BlogCategoryAdmin)
  485. ``wagtail_hooks`` lets you control aspects of the admin, and to expose non-Wagtail models.
  486. In this example, we've specified:
  487. ``add_to_settings_menu = True``
  488. So that our BlogCategories appear in the global Settings menu:
  489. .. figure:: ../_static/images/tutorial/tutorial_11.png
  490. :alt: Adding Blog Categories to Settings
  491. .. figure:: ../_static/images/tutorial/tutorial_12.png
  492. :alt: Categories listing
  493. After using your new Blog Categories interface to create some categories, you can select them from the
  494. InlinePanel in a BlogPage:
  495. .. figure:: ../_static/images/tutorial/tutorial_13.png
  496. :alt: Newly created categories available to a BlogPage
  497. Now that we're storing categories on posts, we need a view to display them, and a way to link to them.
  498. Rather than create another model for the new view, let's consider a category to be a "slice" of data exposed
  499. on the ``BlogIndexPage.`` We can pass a category to the view either as URL parameter: ``/blog?cat=science``
  500. or as a keyword on the end of the URL, which is much cleaner: ``/blog/science``. To access that keyword, we'll
  501. take advantage of Wagtail's :doc:`RoutablePageMixin <../reference/contrib/routablepage>` class. Modify
  502. ``BlogIndexPage`` like this:
  503. .. code-block:: python
  504. from wagtail.contrib.wagtailroutablepage.models import RoutablePageMixin, route
  505. from django.shortcuts import get_object_or_404, render
  506. class BlogIndexPage(RoutablePageMixin, Page):
  507. intro = RichTextField(blank=True)
  508. def get_context(self, request):
  509. # Update context to include only published posts, ordered by reverse-chron
  510. context = super(BlogIndexPage, self).get_context(request)
  511. blogpages = self.get_children().live().order_by('-first_published_at')
  512. # Include queryset of non-empty blog categories for menu
  513. usedcats = BlogCategoryBlogPage.objects.distinct().values_list('category__slug', flat=True)
  514. blogcats = BlogCategory.objects.filter(slug__in=usedcats)
  515. context['blogpages'] = blogpages
  516. context['blogcats'] = blogcats
  517. return context
  518. @route(r'^cat/(\w+)/$', name="blog_category")
  519. def category(self, request, catslug=None):
  520. """
  521. Filter BlogPages by category
  522. """
  523. category = get_object_or_404(BlogCategory, slug=catslug)
  524. blogpages = BlogPage.objects.filter(
  525. blog_categories__category=category).live().order_by('-first_published_at')
  526. context = self.get_context(request)
  527. context['blogpages'] = blogpages
  528. context['category'] = category
  529. return render(request, 'blog/blog_index_page.html', context)
  530. The ``@route`` decorator is new, but as you can see, it works pretty much the same as standard Django URLs,
  531. with a regex pattern matcher and a route name, which we'll use in a minute. The ``^cat...`` in the regex
  532. matches a URL pattern starting at the parent page, so in this case we're matching e.g.
  533. ``/blog/cat/science.`` We query for a ``BlogCategory`` object (or 404), then use it to filter ``BlogPage``
  534. records, traversing through our "ManyToMany" table. Note that when using ``route,``
  535. we need to call Django's ``render()`` manually, specifying the template name.
  536. Since we want to display a nav menu including all non-empty categories, we also insert that queryset
  537. into the context (notice how the ``category()`` suburl calls ``get_context()`` before
  538. appending to context, so the categories list is available on all blog index views.)
  539. Assuming you've created a "Science" category and added some posts to that category, you should now be
  540. able to access a URL like ``/blog/cat/science.`` Now we just need to add category links to our index
  541. and post templates.
  542. We'll also need to be able to reverse blog category links, using a tempate tag
  543. that is not in Wagtail core. In your project settings, add ``'wagtail.contrib.wagtailroutablepage'``
  544. to ``INSTALLED_APPS``, then modify ``blog_index_page.html``:
  545. .. code-block:: html+django
  546. {% extends "base.html" %}
  547. {% load wagtailcore_tags %}
  548. {% load wagtailroutablepage_tags %}
  549. {% block body_class %}template-blogindexpage{% endblock %}
  550. {% block content %}
  551. {% if blogcats %}
  552. <h3>Blog categories:</h3>
  553. <ul>
  554. {% for cat in blogcats %}
  555. <li><a href="{% routablepageurl page "blog_category" cat.slug %}">
  556. {{ cat.name }}</a></li>
  557. {% endfor %}
  558. </ul>
  559. {% endif %}
  560. <h1>{{ page.title }}{% if category %} - {{ category.name }}{% endif %}</h1>
  561. <div class="intro">{{ page.intro|richtext }}</div>
  562. {% for post in blogpages %}
  563. {% with post=post.specific %}
  564. <h2><a href="{% slugurl post.slug %}">{{ post.title }}</a></h2>
  565. {{ post.latest_revision_created_at }}<br />
  566. {% if post.blog_categories.all %}
  567. Filed under:
  568. {% for cat in post.blog_categories.all %}
  569. <a href="{% routablepageurl page "blog_category" cat.category.slug %}">
  570. {{ cat.category.name }}</a>{% if not forloop.last %}, {% endif %}
  571. {% endfor %}<br />
  572. {% endif %}
  573. {% if post.tags.all %}
  574. Tags:
  575. {% for tag in post.tags.all %}
  576. <a href="{% slugurl 'tags' %}?tag={{ tag }}">
  577. <button type="button">{{ tag }}</button></a>
  578. {% endfor %}<br />
  579. {% endif %}
  580. <p>Intro: {{ post.intro }}</p>
  581. {{ post.body|richtext }}
  582. {% endwith %}
  583. {% endfor %}
  584. {% endblock %}
  585. Study the "Filed under:" section -
  586. we loop through each of a blog post's categories (if it has any), and for each, we reverse the URL
  587. to the corresponding blog category view, using the URL we named earlier (``blog_category``), and
  588. passing in the slug of the current category. We also display the category name in the header.
  589. You'll probably want to do something similar on ``blog_page.html``.
  590. And with that, we've got both tags and categories working, and our categories system is nice and dry.
  591. .. figure:: ../_static/images/tutorial/tutorial_14.png
  592. :alt: Blog category view
  593. Where next
  594. ----------
  595. - Read the Wagtail :doc:`topics <../topics/index>` and :doc:`reference <../reference/index>` documentation
  596. - Learn how to implement :doc:`StreamField <../topics/streamfield>` for freeform page content
  597. - Browse through the :doc:`advanced topics <../advanced_topics/index>` section and read :doc:`third-party tutorials <../advanced_topics/third_party_tutorials>`