2
0

tutorial.rst 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888
  1. Your first Wagtail site
  2. =======================
  3. .. note::
  4. This tutorial covers setting up a brand new Wagtail project.
  5. If you'd like to add Wagtail to an existing Django project instead, see :doc:`integrating_into_django`.
  6. Install and run Wagtail
  7. -----------------------
  8. Install dependencies
  9. ~~~~~~~~~~~~~~~~~~~~
  10. Wagtail supports Python 3.5, 3.6, 3.7 and 3.8.
  11. To check whether you have an appropriate version of Python 3:
  12. .. code-block:: console
  13. $ python3 --version
  14. If this does not return a version number or returns a version lower than 3.5, you will need to `install Python 3 <https://www.python.org/downloads/>`_.
  15. .. important::
  16. Before installing Wagtail, it is necessary to install the **libjpeg** and **zlib** libraries, which provide support for working with JPEG, PNG and GIF images (via the Python **Pillow** library).
  17. The way to do this varies by platform—see Pillow's
  18. `platform-specific installation instructions <https://pillow.readthedocs.org/en/latest/installation.html#external-libraries>`_.
  19. Create and activate a virtual environment
  20. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  21. We recommend using a virtual environment, which provides an isolated Python environment.
  22. This tutorial uses `venv <https://docs.python.org/3/tutorial/venv.html>`_, which is packaged with Python 3.
  23. **On Windows** (cmd.exe):
  24. .. code-block:: bat
  25. $ python3 -m venv mysite\env
  26. $ mysite\env\Scripts\activate.bat
  27. **On Unix or MacOS** (bash):
  28. .. code-block:: console
  29. $ python3 -m venv mysite/env
  30. $ source mysite/env/bin/activate
  31. **For other shells** see the `venv documentation <https://docs.python.org/3/library/venv.html>`_.
  32. .. note::
  33. If you're using version control (e.g. git), ``mysite`` will be the directory for your project.
  34. The ``env`` directory inside of it should be excluded from any version control.
  35. Install Wagtail
  36. ~~~~~~~~~~~~~~~
  37. Use pip, which is packaged with Python, to install Wagtail and its dependencies:
  38. .. code-block:: console
  39. $ pip install wagtail
  40. Generate your site
  41. ~~~~~~~~~~~~~~~~~~
  42. Wagtail provides a ``start`` command similar to ``django-admin startproject``.
  43. Running ``wagtail start mysite`` in your project will generate a new ``mysite`` folder with a few Wagtail-specific extras, including
  44. the required project settings,
  45. a "home" app with a blank ``HomePage`` model and basic templates,
  46. and a sample "search" app.
  47. Because the folder ``mysite`` was already created by ``venv``, run ``wagtail start`` with an additional argument to specify the destination directory:
  48. .. code-block:: console
  49. $ wagtail start mysite mysite
  50. Install project dependencies
  51. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  52. .. code-block:: console
  53. $ cd mysite
  54. $ pip install -r requirements.txt
  55. This ensures that you have the relevant versions of
  56. Wagtail,
  57. Django,
  58. and any other dependencies for the project you have just created.
  59. Create the database
  60. ~~~~~~~~~~~~~~~~~~~
  61. If you haven't updated the project settings, this will be a SQLite database file in the project directory.
  62. .. code-block:: console
  63. $ python manage.py migrate
  64. Create an admin user
  65. ~~~~~~~~~~~~~~~~~~~~
  66. .. code-block:: console
  67. $ python manage.py createsuperuser
  68. Start the server
  69. ~~~~~~~~~~~~~~~~
  70. .. code-block:: console
  71. $ python manage.py runserver
  72. If everything worked, http://127.0.0.1:8000 will show you a welcome page:
  73. .. figure:: ../_static/images/tutorial/tutorial_1.png
  74. :alt: Wagtail welcome message
  75. You can now access the administrative area at http://127.0.0.1:8000/admin
  76. .. figure:: ../_static/images/tutorial/tutorial_2.png
  77. :alt: Administrative screen
  78. Extend the HomePage model
  79. -------------------------
  80. 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.
  81. Edit ``home/models.py`` as follows, to add a ``body`` field to the model:
  82. .. code-block:: python
  83. from django.db import models
  84. from wagtail.core.models import Page
  85. from wagtail.core.fields import RichTextField
  86. from wagtail.admin.edit_handlers import FieldPanel
  87. class HomePage(Page):
  88. body = RichTextField(blank=True)
  89. content_panels = Page.content_panels + [
  90. FieldPanel('body', classname="full"),
  91. ]
  92. ``body`` is defined as ``RichTextField``, a special Wagtail field. You
  93. can use any of the :doc:`Django core fields <django:ref/models/fields>`. ``content_panels`` define the
  94. capabilities and the layout of the editing interface. :doc:`More on creating Page models. <../topics/pages>`
  95. Run ``python manage.py makemigrations``, then
  96. ``python manage.py migrate`` to update the database with your model
  97. changes. You must run the above commands each time you make changes to
  98. the model definition.
  99. You can now edit the homepage within the Wagtail admin area (go to Pages, Homepage, then Edit) to see the new body field. Enter some text into the body field, and publish the page.
  100. The page template now needs to be updated to reflect the changes made
  101. to the model. Wagtail uses normal Django templates to render each page
  102. type. By default, it will look for a template filename formed from the app and model name,
  103. separating capital letters with underscores (e.g. HomePage within the 'home' app becomes
  104. ``home/home_page.html``). This template file can exist in any location recognised by
  105. `Django's template rules <https://docs.djangoproject.com/en/stable/intro/tutorial03/#write-views-that-actually-do-something>`__; conventionally it is placed under a ``templates`` folder within the app.
  106. Edit ``home/templates/home/home_page.html`` to contain the following:
  107. .. code-block:: html+django
  108. {% extends "base.html" %}
  109. {% load wagtailcore_tags %}
  110. {% block body_class %}template-homepage{% endblock %}
  111. {% block content %}
  112. {{ page.body|richtext }}
  113. {% endblock %}
  114. .. figure:: ../_static/images/tutorial/tutorial_3.png
  115. :alt: Updated homepage
  116. Wagtail template tags
  117. ~~~~~~~~~~~~~~~~~~~~~
  118. Wagtail provides a number of :ref:`template tags & filters <template-tags-and-filters>`
  119. which can be loaded by including ``{% load wagtailcore_tags %}`` at the top of
  120. your template file.
  121. In this tutorial, we use the `richtext` filter to escape and print the contents
  122. of a ``RichTextField``:
  123. .. code-block:: html+django
  124. {% load wagtailcore_tags %}
  125. {{ page.body|richtext }}
  126. Produces:
  127. .. code-block:: html
  128. <div class="rich-text">
  129. <p>
  130. <b>Welcome</b> to our new site!
  131. </p>
  132. </div>
  133. **Note:** You'll need to include ``{% load wagtailcore_tags %}`` in each
  134. template that uses Wagtail's tags. Django will throw a ``TemplateSyntaxError``
  135. if the tags aren't loaded.
  136. A basic blog
  137. ------------
  138. We are now ready to create a blog. To do so, run
  139. ``python manage.py startapp blog`` to create a new app in your Wagtail site.
  140. Add the new ``blog`` app to ``INSTALLED_APPS`` in ``mysite/settings/base.py``.
  141. Blog Index and Posts
  142. ~~~~~~~~~~~~~~~~~~~~
  143. Lets start with a simple index page for our blog. In ``blog/models.py``:
  144. .. code-block:: python
  145. from wagtail.core.models import Page
  146. from wagtail.core.fields import RichTextField
  147. from wagtail.admin.edit_handlers import FieldPanel
  148. class BlogIndexPage(Page):
  149. intro = RichTextField(blank=True)
  150. content_panels = Page.content_panels + [
  151. FieldPanel('intro', classname="full")
  152. ]
  153. Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
  154. Since the model is called ``BlogIndexPage``, the default template name
  155. (unless we override it) will be ``blog/templates/blog/blog_index_page.html``. Create this file
  156. with the following content:
  157. .. code-block:: html+django
  158. {% extends "base.html" %}
  159. {% load wagtailcore_tags %}
  160. {% block body_class %}template-blogindexpage{% endblock %}
  161. {% block content %}
  162. <h1>{{ page.title }}</h1>
  163. <div class="intro">{{ page.intro|richtext }}</div>
  164. {% for post in page.get_children %}
  165. <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
  166. {{ post.specific.intro }}
  167. {{ post.specific.body|richtext }}
  168. {% endfor %}
  169. {% endblock %}
  170. Most of this should be familiar, but we'll explain ``get_children`` a bit later.
  171. Note the ``pageurl`` tag, which is similar to Django's ``url`` tag but
  172. takes a Wagtail Page object as an argument.
  173. In the Wagtail admin, create a ``BlogIndexPage`` as a child of the Homepage,
  174. make sure it has the slug "blog" on the Promote tab, and publish it.
  175. You should now be able to access the url ``/blog`` on your site
  176. (note how the slug from the Promote tab defines the page URL).
  177. Now we need a model and template for our blog posts. In ``blog/models.py``:
  178. .. code-block:: python
  179. from django.db import models
  180. from wagtail.core.models import Page
  181. from wagtail.core.fields import RichTextField
  182. from wagtail.admin.edit_handlers import FieldPanel
  183. from wagtail.search import index
  184. # Keep the definition of BlogIndexPage, and add:
  185. class BlogPage(Page):
  186. date = models.DateField("Post date")
  187. intro = models.CharField(max_length=250)
  188. body = RichTextField(blank=True)
  189. search_fields = Page.search_fields + [
  190. index.SearchField('intro'),
  191. index.SearchField('body'),
  192. ]
  193. content_panels = Page.content_panels + [
  194. FieldPanel('date'),
  195. FieldPanel('intro'),
  196. FieldPanel('body', classname="full"),
  197. ]
  198. Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
  199. Create a template at ``blog/templates/blog/blog_page.html``:
  200. .. code-block:: html+django
  201. {% extends "base.html" %}
  202. {% load wagtailcore_tags %}
  203. {% block body_class %}template-blogpage{% endblock %}
  204. {% block content %}
  205. <h1>{{ page.title }}</h1>
  206. <p class="meta">{{ page.date }}</p>
  207. <div class="intro">{{ page.intro }}</div>
  208. {{ page.body|richtext }}
  209. <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>
  210. {% endblock %}
  211. Note the use of Wagtail's built-in ``get_parent()`` method to obtain the
  212. URL of the blog this post is a part of.
  213. Now create a few blog posts as children of ``BlogIndexPage``.
  214. Be sure to select type "Blog Page" when creating your posts.
  215. .. figure:: ../_static/images/tutorial/tutorial_4a.png
  216. :alt: Create blog post as child of BlogIndex
  217. .. figure:: ../_static/images/tutorial/tutorial_4b.png
  218. :alt: Choose type BlogPost
  219. Wagtail gives you full control over what kinds of content can be created under
  220. various parent content types. By default, any page type can be a child of any
  221. other page type.
  222. .. figure:: ../_static/images/tutorial/tutorial_5.png
  223. :alt: Page edit screen
  224. You should now have the very beginnings of a working blog.
  225. Access the ``/blog`` URL and you should see something like this:
  226. .. figure:: ../_static/images/tutorial/tutorial_7.png
  227. :alt: Blog basics
  228. Titles should link to post pages, and a link back to the blog's
  229. homepage should appear in the footer of each post page.
  230. Parents and Children
  231. ~~~~~~~~~~~~~~~~~~~~
  232. Much of the work you'll be doing in Wagtail revolves around the concept of hierarchical
  233. "tree" structures consisting of nodes and leaves (see :doc:`../reference/pages/theory`).
  234. In this case, the ``BlogIndexPage`` is a "node" and individual ``BlogPage`` instances
  235. are the "leaves".
  236. Take another look at the guts of ``blog_index_page.html``:
  237. .. code-block:: html+django
  238. {% for post in page.get_children %}
  239. <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
  240. {{ post.specific.intro }}
  241. {{ post.specific.body|richtext }}
  242. {% endfor %}
  243. Every "page" in Wagtail can call out to its parent or children
  244. from its own position in the hierarchy. But why do we have to
  245. specify ``post.specific.intro`` rather than ``post.intro``?
  246. This has to do with the way we defined our model:
  247. ``class BlogPage(Page):``
  248. The ``get_children()`` method gets us a list of instances of the ``Page`` base class.
  249. When we want to reference properties of the instances that inherit from the base class,
  250. Wagtail provides the ``specific`` method that retrieves the actual ``BlogPage`` record.
  251. While the "title" field is present on the base ``Page`` model, "intro" is only present
  252. on the ``BlogPage`` model, so we need ``.specific`` to access it.
  253. To tighten up template code like this, we could use Django's ``with`` tag:
  254. .. code-block:: html+django
  255. {% for post in page.get_children %}
  256. {% with post=post.specific %}
  257. <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
  258. <p>{{ post.intro }}</p>
  259. {{ post.body|richtext }}
  260. {% endwith %}
  261. {% endfor %}
  262. When you start writing more customized Wagtail code, you'll find a whole set of QuerySet
  263. modifiers to help you navigate the hierarchy.
  264. .. code-block:: python
  265. # Given a page object 'somepage':
  266. MyModel.objects.descendant_of(somepage)
  267. child_of(page) / not_child_of(somepage)
  268. ancestor_of(somepage) / not_ancestor_of(somepage)
  269. parent_of(somepage) / not_parent_of(somepage)
  270. sibling_of(somepage) / not_sibling_of(somepage)
  271. # ... and ...
  272. somepage.get_children()
  273. somepage.get_ancestors()
  274. somepage.get_descendants()
  275. somepage.get_siblings()
  276. For more information, see: :doc:`../reference/pages/queryset_reference`
  277. Overriding Context
  278. ~~~~~~~~~~~~~~~~~~
  279. There are a couple of problems with our blog index view:
  280. 1) Blogs generally display content in *reverse* chronological order
  281. 2) We want to make sure we're only displaying *published* content.
  282. To accomplish these things, we need to do more than just grab the index
  283. page's children in the template. Instead, we'll want to modify the
  284. QuerySet in the model definition. Wagtail makes this possible via
  285. the overridable ``get_context()`` method. Modify your ``BlogIndexPage``
  286. model like this:
  287. .. code-block:: python
  288. class BlogIndexPage(Page):
  289. intro = RichTextField(blank=True)
  290. def get_context(self, request):
  291. # Update context to include only published posts, ordered by reverse-chron
  292. context = super().get_context(request)
  293. blogpages = self.get_children().live().order_by('-first_published_at')
  294. context['blogpages'] = blogpages
  295. return context
  296. All we've done here is retrieve the original context, create a custom QuerySet,
  297. add it to the retrieved context, and return the modified context back to the view.
  298. You'll also need to modify your ``blog_index_page.html`` template slightly.
  299. Change:
  300. ``{% for post in page.get_children %}`` to ``{% for post in blogpages %}``
  301. Now try unpublishing one of your posts - it should disappear from the blog index
  302. page. The remaining posts should now be sorted with the most recently published
  303. posts first.
  304. Images
  305. ~~~~~~
  306. Let's add the ability to attach an image gallery to our blog posts. While it's possible to simply insert images into the ``body`` rich text field, there are several advantages to setting up our gallery images as a new dedicated object type within the database - this way, you have full control of the layout and styling of the images on the template, rather than having to lay them out in a particular way within the rich text field. It also makes it possible for the images to be used elsewhere, independently of the blog text - for example, displaying a thumbnail on the blog index page.
  307. Add a new ``BlogPageGalleryImage`` model to ``models.py``:
  308. .. code-block:: python
  309. from django.db import models
  310. # New imports added for ParentalKey, Orderable, InlinePanel, ImageChooserPanel
  311. from modelcluster.fields import ParentalKey
  312. from wagtail.core.models import Page, Orderable
  313. from wagtail.core.fields import RichTextField
  314. from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
  315. from wagtail.images.edit_handlers import ImageChooserPanel
  316. from wagtail.search import index
  317. # ... (Keep the definition of BlogIndexPage, and update BlogPage:)
  318. class BlogPage(Page):
  319. date = models.DateField("Post date")
  320. intro = models.CharField(max_length=250)
  321. body = RichTextField(blank=True)
  322. search_fields = Page.search_fields + [
  323. index.SearchField('intro'),
  324. index.SearchField('body'),
  325. ]
  326. content_panels = Page.content_panels + [
  327. FieldPanel('date'),
  328. FieldPanel('intro'),
  329. FieldPanel('body', classname="full"),
  330. InlinePanel('gallery_images', label="Gallery images"),
  331. ]
  332. class BlogPageGalleryImage(Orderable):
  333. page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='gallery_images')
  334. image = models.ForeignKey(
  335. 'wagtailimages.Image', on_delete=models.CASCADE, related_name='+'
  336. )
  337. caption = models.CharField(blank=True, max_length=250)
  338. panels = [
  339. ImageChooserPanel('image'),
  340. FieldPanel('caption'),
  341. ]
  342. Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
  343. There are a few new concepts here, so let's take them one at a time:
  344. Inheriting from ``Orderable`` adds a ``sort_order`` field to the model, to keep track of the ordering of images in the gallery.
  345. The ``ParentalKey`` to ``BlogPage`` is what attaches the gallery images to a specific page. A ``ParentalKey`` works similarly to a ``ForeignKey``, but also defines ``BlogPageGalleryImage`` as a "child" of the ``BlogPage`` model, so that it's treated as a fundamental part of the page in operations like submitting for moderation, and tracking revision history.
  346. ``image`` is a ``ForeignKey`` to Wagtail's built-in ``Image`` model, where the images themselves are stored. This comes with a dedicated panel type, ``ImageChooserPanel``, which provides a pop-up interface for choosing an existing image or uploading a new one. This way, we allow an image to exist in multiple galleries - effectively, we've created a many-to-many relationship between pages and images.
  347. Specifying ``on_delete=models.CASCADE`` on the foreign key means that if the image is deleted from the system, the gallery entry is deleted as well. (In other situations, it might be appropriate to leave the entry in place - for example, if an "our staff" page included a list of people with headshots, and one of those photos was deleted, we'd rather leave the person in place on the page without a photo. In this case, we'd set the foreign key to ``blank=True, null=True, on_delete=models.SET_NULL``.)
  348. Finally, adding the ``InlinePanel`` to ``BlogPage.content_panels`` makes the gallery images available on the editing interface for ``BlogPage``.
  349. Adjust your blog page template to include the images:
  350. .. code-block:: html+django
  351. {% extends "base.html" %}
  352. {% load wagtailcore_tags wagtailimages_tags %}
  353. {% block body_class %}template-blogpage{% endblock %}
  354. {% block content %}
  355. <h1>{{ page.title }}</h1>
  356. <p class="meta">{{ page.date }}</p>
  357. <div class="intro">{{ page.intro }}</div>
  358. {{ page.body|richtext }}
  359. {% for item in page.gallery_images.all %}
  360. <div style="float: left; margin: 10px">
  361. {% image item.image fill-320x240 %}
  362. <p>{{ item.caption }}</p>
  363. </div>
  364. {% endfor %}
  365. <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>
  366. {% endblock %}
  367. Here we use the ``{% image %}`` tag (which exists in the ``wagtailimages_tags`` library, imported at the top of the template) to insert an ``<img>`` element, with a ``fill-320x240`` parameter to indicate that the image should be resized and cropped to fill a 320x240 rectangle. You can read more about using images in templates in the :doc:`docs <../topics/images>`.
  368. .. figure:: ../_static/images/tutorial/tutorial_6.jpg
  369. :alt: A blog post sample
  370. Since our gallery images are database objects in their own right, we can now query and re-use them independently of the blog post body. Let's define a ``main_image`` method, which returns the image from the first gallery item (or ``None`` if no gallery items exist):
  371. .. code-block:: python
  372. class BlogPage(Page):
  373. date = models.DateField("Post date")
  374. intro = models.CharField(max_length=250)
  375. body = RichTextField(blank=True)
  376. def main_image(self):
  377. gallery_item = self.gallery_images.first()
  378. if gallery_item:
  379. return gallery_item.image
  380. else:
  381. return None
  382. search_fields = Page.search_fields + [
  383. index.SearchField('intro'),
  384. index.SearchField('body'),
  385. ]
  386. content_panels = Page.content_panels + [
  387. FieldPanel('date'),
  388. FieldPanel('intro'),
  389. FieldPanel('body', classname="full"),
  390. InlinePanel('gallery_images', label="Gallery images"),
  391. ]
  392. This method is now available from our templates. Update ``blog_index_page.html`` to include the main image as a thumbnail alongside each post:
  393. .. code-block:: html+django
  394. {% load wagtailcore_tags wagtailimages_tags %}
  395. ...
  396. {% for post in blogpages %}
  397. {% with post=post.specific %}
  398. <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
  399. {% with post.main_image as main_image %}
  400. {% if main_image %}{% image main_image fill-160x100 %}{% endif %}
  401. {% endwith %}
  402. <p>{{ post.intro }}</p>
  403. {{ post.body|richtext }}
  404. {% endwith %}
  405. {% endfor %}
  406. Tagging Posts
  407. ~~~~~~~~~~~~~
  408. Let's say we want to let editors "tag" their posts, so that readers can, e.g.,
  409. view all bicycle-related content together. For this, we'll need to invoke
  410. the tagging system bundled with Wagtail, attach it to the ``BlogPage``
  411. model and content panels, and render linked tags on the blog post template.
  412. Of course, we'll need a working tag-specific URL view as well.
  413. First, alter ``models.py`` once more:
  414. .. code-block:: python
  415. from django.db import models
  416. # New imports added for ClusterTaggableManager, TaggedItemBase, MultiFieldPanel
  417. from modelcluster.fields import ParentalKey
  418. from modelcluster.contrib.taggit import ClusterTaggableManager
  419. from taggit.models import TaggedItemBase
  420. from wagtail.core.models import Page, Orderable
  421. from wagtail.core.fields import RichTextField
  422. from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel
  423. from wagtail.images.edit_handlers import ImageChooserPanel
  424. from wagtail.search import index
  425. # ... (Keep the definition of BlogIndexPage)
  426. class BlogPageTag(TaggedItemBase):
  427. content_object = ParentalKey(
  428. 'BlogPage',
  429. related_name='tagged_items',
  430. on_delete=models.CASCADE
  431. )
  432. class BlogPage(Page):
  433. date = models.DateField("Post date")
  434. intro = models.CharField(max_length=250)
  435. body = RichTextField(blank=True)
  436. tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
  437. # ... (Keep the main_image method and search_fields definition)
  438. content_panels = Page.content_panels + [
  439. MultiFieldPanel([
  440. FieldPanel('date'),
  441. FieldPanel('tags'),
  442. ], heading="Blog information"),
  443. FieldPanel('intro'),
  444. FieldPanel('body'),
  445. InlinePanel('gallery_images', label="Gallery images"),
  446. ]
  447. Run ``python manage.py makemigrations`` and ``python manage.py migrate``.
  448. Note the new ``modelcluster`` and ``taggit`` imports, the addition of a new
  449. ``BlogPageTag`` model, and the addition of a ``tags`` field on ``BlogPage``.
  450. We've also taken the opportunity to use a ``MultiFieldPanel`` in ``content_panels``
  451. to group the date and tags fields together for readability.
  452. Edit one of your ``BlogPage`` instances, and you should now be able to tag posts:
  453. .. figure:: ../_static/images/tutorial/tutorial_8.png
  454. :alt: Tagging a post
  455. To render tags on a ``BlogPage``, add this to ``blog_page.html``:
  456. .. code-block:: html+django
  457. {% if page.tags.all.count %}
  458. <div class="tags">
  459. <h3>Tags</h3>
  460. {% for tag in page.tags.all %}
  461. <a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
  462. {% endfor %}
  463. </div>
  464. {% endif %}
  465. Notice that we're linking to pages here with the builtin ``slugurl``
  466. tag rather than ``pageurl``, which we used earlier. The difference is that ``slugurl`` takes a
  467. Page slug (from the Promote tab) as an argument. ``pageurl`` is more commonly used because it
  468. is unambiguous and avoids extra database lookups. But in the case of this loop, the Page object
  469. isn't readily available, so we fall back on the less-preferred ``slugurl`` tag.
  470. Visiting a blog post with tags should now show a set of linked
  471. buttons at the bottom - one for each tag. However, clicking a button
  472. will get you a 404, since we haven't yet defined a "tags" view. Add to ``models.py``:
  473. .. code-block:: python
  474. class BlogTagIndexPage(Page):
  475. def get_context(self, request):
  476. # Filter by tag
  477. tag = request.GET.get('tag')
  478. blogpages = BlogPage.objects.filter(tags__name=tag)
  479. # Update template context
  480. context = super().get_context(request)
  481. context['blogpages'] = blogpages
  482. return context
  483. Note that this Page-based model defines no fields of its own.
  484. Even without fields, subclassing ``Page`` makes it a part of the
  485. Wagtail ecosystem, so that you can give it a title and URL in the
  486. admin, and so that you can manipulate its contents by returning
  487. a QuerySet from its ``get_context()`` method.
  488. Migrate this in, then create a new ``BlogTagIndexPage`` in the admin.
  489. You'll probably want to create the new page/view as a child of Homepage,
  490. parallel to your Blog index. Give it the slug "tags" on the Promote tab.
  491. Access ``/tags`` and Django will tell you what you probably already knew:
  492. you need to create a template ``blog/blog_tag_index_page.html``:
  493. .. code-block:: html+django
  494. {% extends "base.html" %}
  495. {% load wagtailcore_tags %}
  496. {% block content %}
  497. {% if request.GET.tag|length %}
  498. <h4>Showing pages tagged "{{ request.GET.tag }}"</h4>
  499. {% endif %}
  500. {% for blogpage in blogpages %}
  501. <p>
  502. <strong><a href="{% pageurl blogpage %}">{{ blogpage.title }}</a></strong><br />
  503. <small>Revised: {{ blogpage.latest_revision_created_at }}</small><br />
  504. {% if blogpage.author %}
  505. <p>By {{ blogpage.author.profile }}</p>
  506. {% endif %}
  507. </p>
  508. {% empty %}
  509. No pages found with that tag.
  510. {% endfor %}
  511. {% endblock %}
  512. We're calling the built-in ``latest_revision_created_at`` field on the ``Page``
  513. model - handy to know this is always available.
  514. We haven't yet added an "author" field to our ``BlogPage`` model, nor do we have
  515. a Profile model for authors - we'll leave those as an exercise for the reader.
  516. Clicking the tag button at the bottom of a BlogPost should now render a page
  517. something like this:
  518. .. figure:: ../_static/images/tutorial/tutorial_9.png
  519. :alt: A simple tag view
  520. .. _tutorial_categories:
  521. Categories
  522. ~~~~~~~~~~
  523. Let's add a category system to our blog. Unlike tags, where a page author can bring a tag into existence simply by using it on a page, our categories will be a fixed list, managed by the site owner through a separate area of the admin interface.
  524. First, we define a ``BlogCategory`` model. A category is not a page in its own right, and so we define it as a standard Django ``models.Model`` rather than inheriting from ``Page``. Wagtail introduces the concept of "snippets" for reusable pieces of content that need to be managed through the admin interface, but do not exist as part of the page tree themselves; a model can be registered as a snippet by adding the ``@register_snippet`` decorator. All the field types we've used so far on pages can be used on snippets too - here we'll give each category an icon image as well as a name. Add to ``blog/models.py``:
  525. .. code-block:: python
  526. from wagtail.snippets.models import register_snippet
  527. @register_snippet
  528. class BlogCategory(models.Model):
  529. name = models.CharField(max_length=255)
  530. icon = models.ForeignKey(
  531. 'wagtailimages.Image', null=True, blank=True,
  532. on_delete=models.SET_NULL, related_name='+'
  533. )
  534. panels = [
  535. FieldPanel('name'),
  536. ImageChooserPanel('icon'),
  537. ]
  538. def __str__(self):
  539. return self.name
  540. class Meta:
  541. verbose_name_plural = 'blog categories'
  542. .. note::
  543. Note that we are using ``panels`` rather than ``content_panels`` here - since snippets generally have no need for fields such as slug or publish date, the editing interface for them is not split into separate 'content' / 'promote' / 'settings' tabs as standard, and so there is no need to distinguish between 'content panels' and 'promote panels'.
  544. Migrate this change in, and create a few categories through the Snippets area which now appears in the admin menu.
  545. We can now add categories to the ``BlogPage`` model, as a many-to-many field. The field type we use for this is ``ParentalManyToManyField`` - this is a variant of the standard Django ``ManyToManyField`` which ensures that the chosen objects are correctly stored against the page record in the revision history, in much the same way that ``ParentalKey`` replaces ``ForeignKey`` for one-to-many relations.
  546. .. code-block:: python
  547. # New imports added for forms and ParentalManyToManyField
  548. from django import forms
  549. from django.db import models
  550. from modelcluster.fields import ParentalKey, ParentalManyToManyField
  551. from modelcluster.contrib.taggit import ClusterTaggableManager
  552. from taggit.models import TaggedItemBase
  553. # ...
  554. class BlogPage(Page):
  555. date = models.DateField("Post date")
  556. intro = models.CharField(max_length=250)
  557. body = RichTextField(blank=True)
  558. tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
  559. categories = ParentalManyToManyField('blog.BlogCategory', blank=True)
  560. # ... (Keep the main_image method and search_fields definition)
  561. content_panels = Page.content_panels + [
  562. MultiFieldPanel([
  563. FieldPanel('date'),
  564. FieldPanel('tags'),
  565. FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
  566. ], heading="Blog information"),
  567. FieldPanel('intro'),
  568. FieldPanel('body'),
  569. InlinePanel('gallery_images', label="Gallery images"),
  570. ]
  571. Here we're making use of the ``widget`` keyword argument on the ``FieldPanel`` definition to specify a checkbox-based widget instead of the default multiple select box, as this is often considered more user-friendly.
  572. Finally, we can update the ``blog_page.html`` template to display the categories:
  573. .. code-block:: html+django
  574. <h1>{{ page.title }}</h1>
  575. <p class="meta">{{ page.date }}</p>
  576. {% with categories=page.categories.all %}
  577. {% if categories %}
  578. <h3>Posted in:</h3>
  579. <ul>
  580. {% for category in categories %}
  581. <li style="display: inline">
  582. {% image category.icon fill-32x32 style="vertical-align: middle" %}
  583. {{ category.name }}
  584. </li>
  585. {% endfor %}
  586. </ul>
  587. {% endif %}
  588. {% endwith %}
  589. .. figure:: ../_static/images/tutorial/tutorial_10.jpg
  590. :alt: A blog post with categories
  591. Where next
  592. ----------
  593. - Read the Wagtail :doc:`topics <../topics/index>` and :doc:`reference <../reference/index>` documentation
  594. - Learn how to implement :doc:`StreamField <../topics/streamfield>` for freeform page content
  595. - Browse through the :doc:`advanced topics <../advanced_topics/index>` section and read :doc:`third-party tutorials <../advanced_topics/third_party_tutorials>`