2
0

model_recipes.rst 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. .. _model_recipes:
  2. Recipes
  3. =======
  4. Overriding the :meth:`~wagtail.core.models.Page.serve` Method
  5. --------------------------------------------------------------------
  6. Wagtail defaults to serving :class:`~wagtail.core.models.Page`-derived models by passing a reference to the page object to a Django HTML template matching the model's name, but suppose you wanted to serve something other than HTML? You can override the :meth:`~wagtail.core.models.Page.serve` method provided by the :class:`~wagtail.core.models.Page` class and handle the Django request and response more directly.
  7. Consider this example from the Wagtail demo site's ``models.py``, which serves an ``EventPage`` object as an iCal file if the ``format`` variable is set in the request:
  8. .. code-block:: python
  9. class EventPage(Page):
  10. ...
  11. def serve(self, request):
  12. if "format" in request.GET:
  13. if request.GET['format'] == 'ical':
  14. # Export to ical format
  15. response = HttpResponse(
  16. export_event(self, 'ical'),
  17. content_type='text/calendar',
  18. )
  19. response['Content-Disposition'] = 'attachment; filename=' + self.slug + '.ics'
  20. return response
  21. else:
  22. # Unrecognised format error
  23. message = 'Could not export event\n\nUnrecognised format: ' + request.GET['format']
  24. return HttpResponse(message, content_type='text/plain')
  25. else:
  26. # Display event page as usual
  27. return super().serve(request)
  28. :meth:`~wagtail.core.models.Page.serve` takes a Django request object and returns a Django response object. Wagtail returns a ``TemplateResponse`` object with the template and context which it generates, which allows middleware to function as intended, so keep in mind that a simpler response object like a ``HttpResponse`` will not receive these benefits.
  29. With this strategy, you could use Django or Python utilities to render your model in JSON or XML or any other format you'd like.
  30. .. _overriding_route_method:
  31. Adding Endpoints with Custom :meth:`~wagtail.core.models.Page.route` Methods
  32. -----------------------------------------------------------------------------------
  33. .. note::
  34. A much simpler way of adding more endpoints to pages is provided by the :mod:`~wagtail.contrib.routable_page` module.
  35. Wagtail routes requests by iterating over the path components (separated with a forward slash ``/``), finding matching objects based on their slug, and delegating further routing to that object's model class. The Wagtail source is very instructive in figuring out what's happening. This is the default ``route()`` method of the ``Page`` class:
  36. .. code-block:: python
  37. class Page(...):
  38. ...
  39. def route(self, request, path_components):
  40. if path_components:
  41. # request is for a child of this page
  42. child_slug = path_components[0]
  43. remaining_components = path_components[1:]
  44. # find a matching child or 404
  45. try:
  46. subpage = self.get_children().get(slug=child_slug)
  47. except Page.DoesNotExist:
  48. raise Http404
  49. # delegate further routing
  50. return subpage.specific.route(request, remaining_components)
  51. else:
  52. # request is for this very page
  53. if self.live:
  54. # Return a RouteResult that will tell Wagtail to call
  55. # this page's serve() method
  56. return RouteResult(self)
  57. else:
  58. # the page matches the request, but isn't published, so 404
  59. raise Http404
  60. :meth:`~wagtail.core.models.Page.route` takes the current object (``self``), the ``request`` object, and a list of the remaining ``path_components`` from the request URL. It either continues delegating routing by calling :meth:`~wagtail.core.models.Page.route` again on one of its children in the Wagtail tree, or ends the routing process by returning a ``RouteResult`` object or raising a 404 error.
  61. The ``RouteResult`` object (defined in wagtail.core.url_routing) encapsulates all the information Wagtail needs to call a page's :meth:`~wagtail.core.models.Page.serve` method and return a final response: this information consists of the page object, and any additional ``args``/``kwargs`` to be passed to :meth:`~wagtail.core.models.Page.serve`.
  62. By overriding the :meth:`~wagtail.core.models.Page.route` method, we could create custom endpoints for each object in the Wagtail tree. One use case might be using an alternate template when encountering the ``print/`` endpoint in the path. Another might be a REST API which interacts with the current object. Just to see what's involved, lets make a simple model which prints out all of its child path components.
  63. First, ``models.py``:
  64. .. code-block:: python
  65. from django.shortcuts import render
  66. from wagtail.core.url_routing import RouteResult
  67. from django.http.response import Http404
  68. from wagtail.core.models import Page
  69. ...
  70. class Echoer(Page):
  71. def route(self, request, path_components):
  72. if path_components:
  73. # tell Wagtail to call self.serve() with an additional 'path_components' kwarg
  74. return RouteResult(self, kwargs={'path_components': path_components})
  75. else:
  76. if self.live:
  77. # tell Wagtail to call self.serve() with no further args
  78. return RouteResult(self)
  79. else:
  80. raise Http404
  81. def serve(self, path_components=[]):
  82. return render(request, self.template, {
  83. 'page': self,
  84. 'echo': ' '.join(path_components),
  85. })
  86. This model, ``Echoer``, doesn't define any properties, but does subclass ``Page`` so objects will be able to have a custom title and slug. The template just has to display our ``{{ echo }}`` property.
  87. Now, once creating a new ``Echoer`` page in the Wagtail admin titled "Echo Base," requests such as::
  88. http://127.0.0.1:8000/echo-base/tauntaun/kennel/bed/and/breakfast/
  89. Will return::
  90. tauntaun kennel bed and breakfast
  91. Be careful if you're introducing new required arguments to the ``serve()`` method - Wagtail still needs to be able to display a default view of the page for previewing and moderation, and by default will attempt to do this by calling ``serve()`` with a request object and no further arguments. If your ``serve()`` method does not accept that as a method signature, you will need to override the page's ``serve_preview()`` method to call ``serve()`` with suitable arguments:
  92. .. code-block:: python
  93. def serve_preview(self, request, mode_name):
  94. return self.serve(request, color='purple')
  95. .. _tagging:
  96. Tagging
  97. -------
  98. Wagtail provides tagging capabilities through the combination of two Django modules, `django-taggit <https://django-taggit.readthedocs.io/>`_ (which provides a general-purpose tagging implementation) and `django-modelcluster <https://github.com/wagtail/django-modelcluster>`_ (which extends django-taggit's ``TaggableManager`` to allow tag relations to be managed in memory without writing to the database - necessary for handling previews and revisions). To add tagging to a page model, you'll need to define a 'through' model inheriting from ``TaggedItemBase`` to set up the many-to-many relationship between django-taggit's ``Tag`` model and your page model, and add a ``ClusterTaggableManager`` accessor to your page model to present this relation as a single tag field.
  99. In this example, we set up tagging on ``BlogPage`` through a ``BlogPageTag`` model:
  100. .. code-block:: python
  101. # models.py
  102. from modelcluster.fields import ParentalKey
  103. from modelcluster.contrib.taggit import ClusterTaggableManager
  104. from taggit.models import TaggedItemBase
  105. class BlogPageTag(TaggedItemBase):
  106. content_object = ParentalKey('demo.BlogPage', on_delete=models.CASCADE, related_name='tagged_items')
  107. class BlogPage(Page):
  108. ...
  109. tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
  110. promote_panels = Page.promote_panels + [
  111. ...
  112. FieldPanel('tags'),
  113. ]
  114. Wagtail's admin provides a nice interface for inputting tags into your content, with typeahead tag completion and friendly tag icons.
  115. We can now make use of the many-to-many tag relationship in our views and templates. For example, we can set up the blog's index page to accept a ``?tag=...`` query parameter to filter the ``BlogPage`` listing by tag:
  116. .. code-block:: python
  117. from django.shortcuts import render
  118. class BlogIndexPage(Page):
  119. ...
  120. def get_context(self, request):
  121. context = super().get_context(request)
  122. # Get blog entries
  123. blog_entries = BlogPage.objects.child_of(self).live()
  124. # Filter by tag
  125. tag = request.GET.get('tag')
  126. if tag:
  127. blog_entries = blog_entries.filter(tags__name=tag)
  128. context['blog_entries'] = blog_entries
  129. return context
  130. Here, ``blog_entries.filter(tags__name=tag)`` follows the ``tags`` relation on ``BlogPage``, to filter the listing to only those pages with a matching tag name before passing this to the template for rendering. We can now update the ``blog_page.html`` template to show a list of tags associated with the page, with links back to the filtered index page:
  131. .. code-block:: html+django
  132. {% for tag in page.tags.all %}
  133. <a href="{% pageurl page.blog_index %}?tag={{ tag }}">{{ tag }}</a>
  134. {% endfor %}
  135. Iterating through ``page.tags.all`` will display each tag associated with ``page``, while the links back to the index make use of the filter option added to the ``BlogIndexPage`` model. A Django query could also use the ``tagged_items`` related name field to get ``BlogPage`` objects associated with a tag.
  136. The same approach can be used to add tagging to non-page models managed through :ref:`snippets` and :doc:`/reference/contrib/modeladmin/index`. In this case, the model must inherit from ``modelcluster.models.ClusterableModel`` to be compatible with ``ClusterTaggableManager``.
  137. Custom tag models
  138. -----------------
  139. In the above example, any newly-created tags will be added to django-taggit's default ``Tag`` model, which will be shared by all other models using the same recipe as well as Wagtail's image and document models. In particular, this means that the autocompletion suggestions on tag fields will include tags previously added to other models. To avoid this, you can set up a custom tag model inheriting from ``TagBase``, along with a 'through' model inheriting from ``ItemBase``, which will provide an independent pool of tags for that page model.
  140. .. code-block:: python
  141. from django.db import models
  142. from modelcluster.contrib.taggit import ClusterTaggableManager
  143. from modelcluster.fields import ParentalKey
  144. from taggit.models import TagBase, ItemBase
  145. class BlogTag(TagBase):
  146. class Meta:
  147. verbose_name = "blog tag"
  148. verbose_name_plural = "blog tags"
  149. class TaggedBlog(ItemBase):
  150. tag = models.ForeignKey(
  151. BlogTag, related_name="tagged_blogs", on_delete=models.CASCADE
  152. )
  153. content_object = ParentalKey(
  154. to='demo.BlogPage',
  155. on_delete=models.CASCADE,
  156. related_name='tagged_items'
  157. )
  158. class BlogPage(Page):
  159. ...
  160. tags = ClusterTaggableManager(through='demo.TaggedBlog', blank=True)
  161. Within the admin, the tag field will automatically recognise the custom tag model being used, and will offer autocomplete suggestions taken from that tag model.
  162. Disabling free tagging
  163. ----------------------
  164. By default, tag fields work on a "free tagging" basis: editors can enter anything into the field, and upon saving, any tag text not recognised as an existing tag will be created automatically. To disable this behaviour, and only allow editors to enter tags that already exist in the database, custom tag models accept a ``free_tagging = False`` option:
  165. .. code-block:: python
  166. from taggit.models import TagBase
  167. from wagtail.snippets.models import register_snippet
  168. @register_snippet
  169. class BlogTag(TagBase):
  170. free_tagging = False
  171. class Meta:
  172. verbose_name = "blog tag"
  173. verbose_name_plural = "blog tags"
  174. Here we have registered ``BlogTag`` as a snippet, to provide an interface for administrators (and other users with the appropriate permissions) to manage the allowed set of tags. With the ``free_tagging = False`` option set, editors can no longer enter arbitrary text into the tag field, and must instead select existing tags from the autocomplete dropdown.
  175. .. _page_model_auto_redirects_recipe:
  176. Have redirects created automatically when changing page slug
  177. ------------------------------------------------------------
  178. You may want redirects created automatically when a url gets changed in the admin so as to avoid broken links. You can add something like the following block to a ``wagtail_hooks.py`` file within one of your project's apps.
  179. .. code-block:: python
  180. from wagtail.core import hooks
  181. from wagtail.contrib.redirects.models import Redirect
  182. # Create redirect when editing slugs
  183. @hooks.register('before_edit_page')
  184. def create_redirect_on_slug_change(request, page):
  185. if request.method == 'POST':
  186. if page.slug != request.POST['slug']:
  187. Redirect.objects.create(
  188. old_path=page.url[:-1],
  189. site=page.get_site(),
  190. redirect_page=page
  191. )
  192. Note: This does not work in some cases e.g. when you redirect a page, create a new page in that url and then move the new one. It should be helpful in most cases however.