hooks.rst 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514
  1. .. _admin_hooks:
  2. Hooks
  3. =====
  4. On loading, Wagtail will search for any app with the file ``wagtail_hooks.py`` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a page is saved or when the main menu is constructed.
  5. .. note::
  6. Hooks are typically used to customise the view-level behaviour of the Wagtail admin and front-end. For customisations that only deal with model-level behaviour - such as calling an external service when a page or document is added - it is often better to use :doc:`Django's signal mechanism <django:topics/signals>` (see also: :ref:`Wagtail signals <signals>`), as these are not dependent on a user taking a particular path through the admin interface.
  7. Registering functions with a Wagtail hook is done through the ``@hooks.register`` decorator:
  8. .. code-block:: python
  9. from wagtail.core import hooks
  10. @hooks.register('name_of_hook')
  11. def my_hook_function(arg1, arg2...)
  12. # your code here
  13. Alternatively, ``hooks.register`` can be called as an ordinary function, passing in the name of the hook and a handler function defined elsewhere:
  14. .. code-block:: python
  15. hooks.register('name_of_hook', my_hook_function)
  16. If you need your hooks to run in a particular order, you can pass the ``order`` parameter. If order is not specified then the hooks proceed in the order given by INSTALLED_APPS. Wagtail uses hooks internally, too, so you need to be aware of order when overriding built-in Wagtail functionality (i.e. removing default summary items):
  17. .. code-block:: python
  18. @hooks.register('name_of_hook', order=1) # This will run after every hook in the wagtail core
  19. def my_hook_function(arg1, arg2...)
  20. # your code here
  21. @hooks.register('name_of_hook', order=-1) # This will run before every hook in the wagtail core
  22. def my_other_hook_function(arg1, arg2...)
  23. # your code here
  24. @hooks.register('name_of_hook', order=2) # This will run after `my_hook_function`
  25. def yet_another_hook_function(arg1, arg2...)
  26. # your code here
  27. Unit testing hooks
  28. ------------------
  29. Hooks are usually registered on startup and can't be changed at runtime. But when writing unit tests, you might want to register a hook
  30. function just for a single test or block of code and unregister it so that it doesn't run when other tests are run.
  31. You can register hooks temporarily using the ``hooks.register_temporarily`` function, this can be used as both a decorator and a context
  32. manager. Here's an example of how to register a hook function for just a single test:
  33. .. code-block:: python
  34. def my_hook_function():
  35. ...
  36. class MyHookTest(TestCase):
  37. @hooks.register_temporarily('name_of_hook', my_hook_function)
  38. def test_my_hook_function(self):
  39. # Test with the hook registered here
  40. ...
  41. And here's an example of registering a hook function for a single block of code:
  42. .. code-block:: python
  43. def my_hook_function():
  44. ...
  45. with hooks.register_temporarily('name_of_hook', my_hook_function):
  46. # Hook is registered here
  47. ..
  48. # Hook is unregistered here
  49. If you need to register multiple hooks in a ``with`` block, you can pass the hooks in as a list of tuples:
  50. .. code-block:: python
  51. def my_hook(...):
  52. pass
  53. def my_other_hook(...):
  54. pass
  55. with hooks.register_temporarily([
  56. ('hook_name', my_hook),
  57. ('hook_name', my_other_hook),
  58. ]):
  59. # All hooks are registered here
  60. ..
  61. # All hooks are unregistered here
  62. The available hooks are listed below.
  63. .. contents::
  64. :local:
  65. :depth: 1
  66. Admin modules
  67. -------------
  68. Hooks for building new areas of the admin interface (alongside pages, images, documents and so on).
  69. .. _construct_homepage_panels:
  70. ``construct_homepage_panels``
  71. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  72. Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a ``request`` object and a list of panel objects, and should modify this list in-place as required. Panel objects are :doc:`components </extending/template_components>` with an additional ``order`` property, an integer that determines the panel's position in the final ordered list. The default panels use integers between ``100`` and ``300``.
  73. .. code-block:: python
  74. from django.utils.safestring import mark_safe
  75. from wagtail.admin.ui.components import Component
  76. from wagtail.core import hooks
  77. class WelcomePanel(Component):
  78. order = 50
  79. def render_html(self, parent_context):
  80. return mark_safe("""
  81. <section class="panel summary nice-padding">
  82. <h3>No, but seriously -- welcome to the admin homepage.</h3>
  83. </section>
  84. """)
  85. @hooks.register('construct_homepage_panels')
  86. def add_another_welcome_panel(request, panels):
  87. panels.append(WelcomePanel())
  88. .. _construct_homepage_summary_items:
  89. ``construct_homepage_summary_items``
  90. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  91. Add or remove items from the 'site summary' bar on the admin homepage (which shows the number of pages and other object that exist on the site). The callable passed into this hook should take a ``request`` object and a list of summary item objects, and should modify this list in-place as required. Summary item objects are instances of ``wagtail.admin.site_summary.SummaryItem``, which extends :ref:`the Component class <creating_template_components>` with the following additional methods and properties:
  92. .. method:: SummaryItem(request)
  93. Constructor; receives the request object its argument
  94. .. attribute:: order
  95. An integer that specifies the item's position in the sequence.
  96. .. method:: is_shown()
  97. Returns a boolean indicating whether the summary item should be shown on this request.
  98. .. _construct_main_menu:
  99. ``construct_main_menu``
  100. ~~~~~~~~~~~~~~~~~~~~~~~
  101. Called just before the Wagtail admin menu is output, to allow the list of menu items to be modified. The callable passed to this hook will receive a ``request`` object and a list of ``menu_items``, and should modify ``menu_items`` in-place as required. Adding menu items should generally be done through the ``register_admin_menu_item`` hook instead - items added through ``construct_main_menu`` will be missing any associated JavaScript includes, and their ``is_shown`` check will not be applied.
  102. .. code-block:: python
  103. from wagtail.core import hooks
  104. @hooks.register('construct_main_menu')
  105. def hide_explorer_menu_item_from_frank(request, menu_items):
  106. if request.user.username == 'frank':
  107. menu_items[:] = [item for item in menu_items if item.name != 'explorer']
  108. .. _describe_collection_contents:
  109. ``describe_collection_contents``
  110. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  111. Called when Wagtail needs to find out what objects exist in a collection, if any. Currently this happens on the confirmation before deleting a collection, to ensure that non-empty collections cannot be deleted. The callable passed to this hook will receive a ``collection`` object, and should return either ``None`` (to indicate no objects in this collection), or a dict containing the following keys:
  112. ``count``
  113. A numeric count of items in this collection
  114. ``count_text``
  115. A human-readable string describing the number of items in this collection, such as "3 documents". (Sites with multi-language support should return a translatable string here, most likely using the ``django.utils.translation.ngettext`` function.)
  116. ``url`` (optional)
  117. A URL to an index page that lists the objects being described.
  118. ``register_account_settings_panel``
  119. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  120. Registers a new settings panel class to add to the "Account" view in the admin.
  121. This hook can be added to a sub-class of ``BaseSettingsPanel``. For example:
  122. .. code-block:: python
  123. from wagtail.admin.views.account import BaseSettingsPanel
  124. from wagtail.core import hooks
  125. @hooks.register('register_account_settings_panel')
  126. class CustomSettingsPanel(BaseSettingsPanel):
  127. name = 'custom'
  128. title = "My custom settings"
  129. order = 500
  130. form_class = CustomSettingsForm
  131. Alternatively, it can also be added to a function. For example, this function is equivalent to the above:
  132. .. code-block:: python
  133. from wagtail.admin.views.account import BaseSettingsPanel
  134. from wagtail.core import hooks
  135. class CustomSettingsPanel(BaseSettingsPanel):
  136. name = 'custom'
  137. title = "My custom settings"
  138. order = 500
  139. form_class = CustomSettingsForm
  140. @hooks.register('register_account_settings_panel')
  141. def register_custom_settings_panel(request, user, profile):
  142. return CustomSettingsPanel(request, user, profile)
  143. More details about the options that are available can be found at :doc:`/extending/custom_account_settings`.
  144. .. _register_account_menu_item:
  145. ``register_account_menu_item``
  146. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  147. Add an item to the "More actions" tab on the "Account" page within the Wagtail admin.
  148. The callable for this hook should return a dict with the keys
  149. ``url``, ``label`` and ``help_text``. For example:
  150. .. code-block:: python
  151. from django.urls import reverse
  152. from wagtail.core import hooks
  153. @hooks.register('register_account_menu_item')
  154. def register_account_delete_account(request):
  155. return {
  156. 'url': reverse('delete-account'),
  157. 'label': 'Delete account',
  158. 'help_text': 'This permanently deletes your account.'
  159. }
  160. .. _register_admin_menu_item:
  161. ``register_admin_menu_item``
  162. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  163. Add an item to the Wagtail admin menu. The callable passed to this hook must return an instance of ``wagtail.admin.menu.MenuItem``. New items can be constructed from the ``MenuItem`` class by passing in a ``label`` which will be the text in the menu item, and the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up). Additionally, the following keyword arguments are accepted:
  164. :name: an internal name used to identify the menu item; defaults to the slugified form of the label.
  165. :icon_name: icon to display against the menu item
  166. :classnames: additional classnames applied to the link
  167. :attrs: additional HTML attributes to apply to the link
  168. :order: an integer which determines the item's position in the menu
  169. For menu items that are only available to superusers, the subclass ``wagtail.admin.menu.AdminOnlyMenuItem`` can be used in place of ``MenuItem``.
  170. ``MenuItem`` can be further subclassed to customise the HTML output, specify JavaScript files required by the menu item, or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (``wagtail/admin/menu.py``) for details.
  171. .. code-block:: python
  172. from django.urls import reverse
  173. from wagtail.core import hooks
  174. from wagtail.admin.menu import MenuItem
  175. @hooks.register('register_admin_menu_item')
  176. def register_frank_menu_item():
  177. return MenuItem('Frank', reverse('frank'), icon_name='folder-inverse', order=10000)
  178. .. _register_admin_urls:
  179. ``register_admin_urls``
  180. ~~~~~~~~~~~~~~~~~~~~~~~
  181. Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see :doc:`url dispatcher <django:topics/http/urls>`.
  182. .. code-block:: python
  183. from django.http import HttpResponse
  184. from django.urls import path
  185. from wagtail.core import hooks
  186. def admin_view(request):
  187. return HttpResponse(
  188. "I have approximate knowledge of many things!",
  189. content_type="text/plain")
  190. @hooks.register('register_admin_urls')
  191. def urlconf_time():
  192. return [
  193. path('how_did_you_almost_know_my_name/', admin_view, name='frank'),
  194. ]
  195. .. _register_group_permission_panel:
  196. ``register_group_permission_panel``
  197. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  198. Add a new panel to the Groups form in the 'settings' area. The callable passed to this hook must return a ModelForm / ModelFormSet-like class, with a constructor that accepts a group object as its ``instance`` keyword argument, and which implements the methods ``save``, ``is_valid``, and ``as_admin_panel`` (which returns the HTML to be included on the group edit page).
  199. .. _register_settings_menu_item:
  200. ``register_settings_menu_item``
  201. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  202. As ``register_admin_menu_item``, but registers menu items into the 'Settings' sub-menu rather than the top-level menu.
  203. .. _construct_settings_menu:
  204. ``construct_settings_menu``
  205. ~~~~~~~~~~~~~~~~~~~~~~~~~~~
  206. As ``construct_main_menu``, but modifies the 'Settings' sub-menu rather than the top-level menu.
  207. .. _register_reports_menu_item:
  208. ``register_reports_menu_item``
  209. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  210. As ``register_admin_menu_item``, but registers menu items into the 'Reports' sub-menu rather than the top-level menu.
  211. .. _construct_reports_menu:
  212. ``construct_reports_menu``
  213. ~~~~~~~~~~~~~~~~~~~~~~~~~~~
  214. As ``construct_main_menu``, but modifies the 'Reports' sub-menu rather than the top-level menu.
  215. .. _register_admin_search_area:
  216. ``register_admin_search_area``
  217. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  218. Add an item to the Wagtail admin search "Other Searches". Behaviour of this hook is similar to ``register_admin_menu_item``. The callable passed to this hook must return an instance of ``wagtail.admin.search.SearchArea``. New items can be constructed from the ``SearchArea`` class by passing the following parameters:
  219. :label: text displayed in the "Other Searches" option box.
  220. :name: an internal name used to identify the search option; defaults to the slugified form of the label.
  221. :url: the URL of the target search page.
  222. :classnames: arbitrary CSS classnames applied to the link
  223. :icon_name: icon to display next to the label.
  224. :attrs: additional HTML attributes to apply to the link.
  225. :order: an integer which determines the item's position in the list of options.
  226. Setting the URL can be achieved using reverse() on the target search page. The GET parameter 'q' will be appended to the given URL.
  227. A template tag, ``search_other`` is provided by the ``wagtailadmin_tags`` template module. This tag takes a single, optional parameter, ``current``, which allows you to specify the ``name`` of the search option currently active. If the parameter is not given, the hook defaults to a reverse lookup of the page's URL for comparison against the ``url`` parameter.
  228. ``SearchArea`` can be subclassed to customise the HTML output, specify JavaScript files required by the option, or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (``wagtail/admin/search.py``) for details.
  229. .. code-block:: python
  230. from django.urls import reverse
  231. from wagtail.core import hooks
  232. from wagtail.admin.search import SearchArea
  233. @hooks.register('register_admin_search_area')
  234. def register_frank_search_area():
  235. return SearchArea('Frank', reverse('frank'), icon_name='folder-inverse', order=10000)
  236. .. _register_permissions:
  237. ``register_permissions``
  238. ~~~~~~~~~~~~~~~~~~~~~~~~
  239. Return a QuerySet of ``Permission`` objects to be shown in the Groups administration area.
  240. .. code-block:: python
  241. from django.contrib.auth.models import Permission
  242. from wagtail.core import hooks
  243. @hooks.register('register_permissions')
  244. def register_permissions():
  245. app = 'blog'
  246. model = 'extramodelset'
  247. return Permission.objects.filter(content_type__app_label=app, codename__in=[
  248. f"view_{model}", f"add_{model}", f"change_{model}", f"delete_{model}"
  249. ])
  250. .. _filter_form_submissions_for_user:
  251. ``filter_form_submissions_for_user``
  252. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  253. Allows access to form submissions to be customised on a per-user, per-form basis.
  254. This hook takes two parameters:
  255. - The user attempting to access form submissions
  256. - A ``QuerySet`` of form pages
  257. The hook must return a ``QuerySet`` containing a subset of these form pages which the user is allowed to access the submissions for.
  258. For example, to prevent non-superusers from accessing form submissions:
  259. .. code-block:: python
  260. from wagtail.core import hooks
  261. @hooks.register('filter_form_submissions_for_user')
  262. def construct_forms_for_user(user, queryset):
  263. if not user.is_superuser:
  264. queryset = queryset.none()
  265. return queryset
  266. Editor interface
  267. ----------------
  268. Hooks for customising the editing interface for pages and snippets.
  269. .. _register_rich_text_features:
  270. ``register_rich_text_features``
  271. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  272. Rich text fields in Wagtail work with a list of 'feature' identifiers that determine which editing controls are available in the editor, and which elements are allowed in the output; for example, a rich text field defined as ``RichTextField(features=['h2', 'h3', 'bold', 'italic', 'link'])`` would allow headings, bold / italic formatting and links, but not (for example) bullet lists or images. The ``register_rich_text_features`` hook allows new feature identifiers to be defined - see :ref:`rich_text_features` for details.
  273. .. _insert_editor_css:
  274. ``insert_editor_css``
  275. ~~~~~~~~~~~~~~~~~~~~~
  276. Add additional CSS files or snippets to the page editor.
  277. .. code-block:: python
  278. from django.templatetags.static import static
  279. from django.utils.html import format_html
  280. from wagtail.core import hooks
  281. @hooks.register('insert_editor_css')
  282. def editor_css():
  283. return format_html(
  284. '<link rel="stylesheet" href="{}">',
  285. static('demo/css/vendor/font-awesome/css/font-awesome.min.css')
  286. )
  287. .. _insert_global_admin_css:
  288. ``insert_global_admin_css``
  289. ~~~~~~~~~~~~~~~~~~~~~~~~~~~
  290. Add additional CSS files or snippets to all admin pages.
  291. .. code-block:: python
  292. from django.utils.html import format_html
  293. from django.templatetags.static import static
  294. from wagtail.core import hooks
  295. @hooks.register('insert_global_admin_css')
  296. def global_admin_css():
  297. return format_html('<link rel="stylesheet" href="{}">', static('my/wagtail/theme.css'))
  298. .. _insert_editor_js:
  299. ``insert_editor_js``
  300. ~~~~~~~~~~~~~~~~~~~~
  301. Add additional JavaScript files or code snippets to the page editor.
  302. .. code-block:: python
  303. from django.utils.html import format_html_join
  304. from django.utils.safestring import mark_safe
  305. from django.templatetags.static import static
  306. from wagtail.core import hooks
  307. @hooks.register('insert_editor_js')
  308. def editor_js():
  309. js_files = [
  310. 'demo/js/jquery.raptorize.1.0.js',
  311. ]
  312. js_includes = format_html_join('\n', '<script src="{0}"></script>',
  313. ((static(filename),) for filename in js_files)
  314. )
  315. return js_includes + mark_safe(
  316. """
  317. <script>
  318. $(function() {
  319. $('button').raptorize();
  320. });
  321. </script>
  322. """
  323. )
  324. .. _insert_global_admin_js:
  325. ``insert_global_admin_js``
  326. ~~~~~~~~~~~~~~~~~~~~~~~~~~
  327. Add additional JavaScript files or code snippets to all admin pages.
  328. .. code-block:: python
  329. from django.utils.html import format_html
  330. from wagtail.core import hooks
  331. @hooks.register('insert_global_admin_js')
  332. def global_admin_js():
  333. return format_html(
  334. '<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.js"></script>',
  335. )
  336. Editor workflow
  337. ---------------
  338. Hooks for customising the way users are directed through the process of creating page content.
  339. .. _after_create_page:
  340. ``after_create_page``
  341. ~~~~~~~~~~~~~~~~~~~~~
  342. Do something with a ``Page`` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a ``request`` object and a ``page`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page's parent.
  343. .. code-block:: python
  344. from django.http import HttpResponse
  345. from wagtail.core import hooks
  346. @hooks.register('after_create_page')
  347. def do_after_page_create(request, page):
  348. return HttpResponse("Congrats on making content!", content_type="text/plain")
  349. If you set attributes on a ``Page`` object, you should also call ``save_revision()``, since the edit and index view pick up their data from the revisions table rather than the actual saved page record.
  350. .. code-block:: python
  351. @hooks.register('after_create_page')
  352. def set_attribute_after_page_create(request, page):
  353. page.title = 'Persistent Title'
  354. new_revision = page.save_revision()
  355. if page.live:
  356. # page has been created and published at the same time,
  357. # so ensure that the updated title is on the published version too
  358. new_revision.publish()
  359. .. _before_create_page:
  360. ``before_create_page``
  361. ~~~~~~~~~~~~~~~~~~~~~~
  362. Called at the beginning of the "create page" view passing in the request, the parent page and page model class.
  363. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
  364. Unlike, ``after_create_page``, this is run both for both ``GET`` and ``POST`` requests.
  365. This can be used to completely override the editor on a per-view basis:
  366. .. code-block:: python
  367. from wagtail.core import hooks
  368. from .models import AwesomePage
  369. from .admin_views import edit_awesome_page
  370. @hooks.register('before_create_page')
  371. def before_create_page(request, parent_page, page_class):
  372. # Use a custom create view for the AwesomePage model
  373. if page_class == AwesomePage:
  374. return create_awesome_page(request, parent_page)
  375. .. _after_delete_page:
  376. ``after_delete_page``
  377. ~~~~~~~~~~~~~~~~~~~~~
  378. Do something after a ``Page`` object is deleted. Uses the same behaviour as ``after_create_page``.
  379. .. _before_delete_page:
  380. ``before_delete_page``
  381. ~~~~~~~~~~~~~~~~~~~~~~
  382. Called at the beginning of the "delete page" view passing in the request and the page object.
  383. Uses the same behaviour as ``before_create_page``, is is run both for both ``GET`` and ``POST`` requests.
  384. .. code-block:: python
  385. from django.shortcuts import redirect
  386. from django.utils.html import format_html
  387. from wagtail.admin import messages
  388. from wagtail.core import hooks
  389. from .models import AwesomePage
  390. @hooks.register('before_delete_page')
  391. def before_delete_page(request, page):
  392. """Block awesome page deletion and show a message."""
  393. if request.method == 'POST' and page.specific_class in [AwesomePage]:
  394. messages.warning(request, "Awesome pages cannot be deleted, only unpublished")
  395. return redirect('wagtailadmin_pages:delete', page.pk)
  396. .. _after_edit_page:
  397. ``after_edit_page``
  398. ~~~~~~~~~~~~~~~~~~~
  399. Do something with a ``Page`` object after it has been updated. Uses the same behaviour as ``after_create_page``.
  400. .. _before_edit_page:
  401. ``before_edit_page``
  402. ~~~~~~~~~~~~~~~~~~~~~
  403. Called at the beginning of the "edit page" view passing in the request and the page object.
  404. Uses the same behaviour as ``before_create_page``.
  405. .. _after_publish_page:
  406. ``after_publish_page``
  407. ~~~~~~~~~~~~~~~~~~~~~~~~
  408. Do something with a ``Page`` object after it has been published via page create view or page edit view.
  409. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
  410. .. _before_publish_page:
  411. ``before_publish_page``
  412. ~~~~~~~~~~~~~~~~~~~~~~~~~
  413. Do something with a ``Page`` object before it has been published via page create view or page edit view.
  414. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
  415. .. _after_unpublish_page:
  416. ``after_unpublish_page``
  417. ~~~~~~~~~~~~~~~~~~~~~~~~
  418. Called after unpublish action in "unpublish" view passing in the request and the page object.
  419. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
  420. .. _before_unpublish_page:
  421. ``before_unpublish_page``
  422. ~~~~~~~~~~~~~~~~~~~~~~~~~
  423. Called before unpublish action in "unpublish" view passing in the request and the page object.
  424. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
  425. .. _after_copy_page:
  426. ``after_copy_page``
  427. ~~~~~~~~~~~~~~~~~~~
  428. Do something with a ``Page`` object after it has been copied passing in the request, page object and the new copied page. Uses the same behaviour as ``after_create_page``.
  429. .. _before_copy_page:
  430. ``before_copy_page``
  431. ~~~~~~~~~~~~~~~~~~~~~
  432. Called at the beginning of the "copy page" view passing in the request and the page object.
  433. Uses the same behaviour as ``before_create_page``.
  434. .. _after_move_page:
  435. ``after_move_page``
  436. ~~~~~~~~~~~~~~~~~~~
  437. Do something with a ``Page`` object after it has been moved passing in the request and page object. Uses the same behaviour as ``after_create_page``.
  438. .. _before_move_page:
  439. ``before_move_page``
  440. ~~~~~~~~~~~~~~~~~~~~~
  441. Called at the beginning of the "move page" view passing in the request, the page object and the destination page object.
  442. Uses the same behaviour as ``before_create_page``.
  443. .. _before_convert_alias_page:
  444. ``before_convert_alias_page``
  445. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  446. Called at the beginning of the ``convert_alias`` view, which is responsible for converting alias pages into normal Wagtail pages.
  447. The request and the page being converted are passed in as arguments to the hook.
  448. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
  449. .. _after_convert_alias_page:
  450. ``after_convert_alias_page``
  451. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  452. Do something with a ``Page`` object after it has been converted from an alias.
  453. The request and the page that was just converted are passed in as arguments to the hook.
  454. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
  455. .. _register_page_action_menu_item:
  456. ``register_page_action_menu_item``
  457. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  458. Add an item to the popup menu of actions on the page creation and edit views. The callable passed to this hook must return an instance of ``wagtail.admin.action_menu.ActionMenuItem``. ``ActionMenuItem`` is a subclass of :ref:`Component <creating_template_components>` and so the rendering of the menu item can be customised through ``template_name``, ``get_context_data``, ``render_html`` and ``Media``. In addition, the following attributes and methods are available to be overridden:
  459. :order: an integer (default 100) which determines the item's position in the menu. Can also be passed as a keyword argument to the object constructor. The lowest-numbered item in this sequence will be selected as the default menu item; as standard, this is "Save draft" (which has an ``order`` of 0).
  460. :label: the displayed text of the menu item
  461. :get_url: a method which returns a URL for the menu item to link to; by default, returns ``None`` which causes the menu item to behave as a form submit button instead
  462. :name: value of the ``name`` attribute of the submit button, if no URL is specified
  463. :icon_name: icon to display against the menu item
  464. :classname: a ``class`` attribute value to add to the button element
  465. :is_shown: a method which returns a boolean indicating whether the menu item should be shown; by default, true except when editing a locked page
  466. The ``get_url``, ``is_shown``, ``get_context_data`` and ``render_html`` methods all accept a context dictionary containing the following fields:
  467. :view: name of the current view: ``'create'``, ``'edit'`` or ``'revisions_revert'``
  468. :page: For ``view`` = ``'edit'`` or ``'revisions_revert'``, the page being edited
  469. :parent_page: For ``view`` = ``'create'``, the parent page of the page being created
  470. :request: The current request object
  471. :user_page_permissions: a ``UserPagePermissionsProxy`` object for the current user, to test permissions against
  472. .. code-block:: python
  473. from wagtail.core import hooks
  474. from wagtail.admin.action_menu import ActionMenuItem
  475. class GuacamoleMenuItem(ActionMenuItem):
  476. name = 'action-guacamole'
  477. label = "Guacamole"
  478. def get_url(self, context):
  479. return "https://www.youtube.com/watch?v=dNJdJIwCF_Y"
  480. @hooks.register('register_page_action_menu_item')
  481. def register_guacamole_menu_item():
  482. return GuacamoleMenuItem(order=10)
  483. .. _construct_page_action_menu:
  484. ``construct_page_action_menu``
  485. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  486. Modify the final list of action menu items on the page creation and edit views. The callable passed to this hook receives a list of ``ActionMenuItem`` objects, a request object and a context dictionary as per ``register_page_action_menu_item``, and should modify the list of menu items in-place.
  487. .. code-block:: python
  488. @hooks.register('construct_page_action_menu')
  489. def remove_submit_to_moderator_option(menu_items, request, context):
  490. menu_items[:] = [item for item in menu_items if item.name != 'action-submit']
  491. The ``construct_page_action_menu`` hook is called after the menu items have been sorted by their order attributes, and so setting a menu item's order will have no effect at this point. Instead, items can be reordered by changing their position in the list, with the first item being selected as the default action. For example, to change the default action to Publish:
  492. .. code-block:: python
  493. @hooks.register('construct_page_action_menu')
  494. def make_publish_default_action(menu_items, request, context):
  495. for (index, item) in enumerate(menu_items):
  496. if item.name == 'action-publish':
  497. # move to top of list
  498. menu_items.pop(index)
  499. menu_items.insert(0, item)
  500. break
  501. .. construct_page_listing_buttons:
  502. ``construct_page_listing_buttons``
  503. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  504. Modify the final list of page listing buttons in the page explorer. The
  505. callable passed to this hook receives a list of ``PageListingButton`` objects, a page,
  506. a page perms object, and a context dictionary as per ``register_page_listing_buttons``,
  507. and should modify the list of listing items in-place.
  508. .. code-block:: python
  509. @hooks.register('construct_page_listing_buttons')
  510. def remove_page_listing_button_item(buttons, page, page_perms, is_parent=False, context=None):
  511. if is_parent:
  512. buttons.pop() # removes the last 'more' dropdown button on the parent page listing buttons
  513. .. _construct_wagtail_userbar:
  514. ``construct_wagtail_userbar``
  515. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  516. Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the ``request`` object and a list of menu objects, ``items``. The menu item objects must have a ``render`` method which can take a ``request`` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information.
  517. .. code-block:: python
  518. from wagtail.core import hooks
  519. class UserbarPuppyLinkItem:
  520. def render(self, request):
  521. return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
  522. + 'target="_parent" role="menuitem" class="action icon icon-wagtail">Puppies!</a></li>'
  523. @hooks.register('construct_wagtail_userbar')
  524. def add_puppy_link_item(request, items):
  525. return items.append( UserbarPuppyLinkItem() )
  526. Admin workflow
  527. --------------
  528. Hooks for customising the way admins are directed through the process of editing users.
  529. .. _after_create_user:
  530. ``after_create_user``
  531. ~~~~~~~~~~~~~~~~~~~~~
  532. Do something with a ``User`` object after it has been saved to the database. The callable passed to this hook should take a ``request`` object and a ``user`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the User index page.
  533. .. code-block:: python
  534. from django.http import HttpResponse
  535. from wagtail.core import hooks
  536. @hooks.register('after_create_user')
  537. def do_after_page_create(request, user):
  538. return HttpResponse("Congrats on creating a new user!", content_type="text/plain")
  539. .. _before_create_user:
  540. ``before_create_user``
  541. ~~~~~~~~~~~~~~~~~~~~~~
  542. Called at the beginning of the "create user" view passing in the request.
  543. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object and skip the rest of the view.
  544. Unlike, ``after_create_user``, this is run both for both ``GET`` and ``POST`` requests.
  545. This can be used to completely override the user editor on a per-view basis:
  546. .. code-block:: python
  547. from django.http import HttpResponse
  548. from wagtail.core import hooks
  549. from .models import AwesomePage
  550. from .admin_views import edit_awesome_page
  551. @hooks.register('before_create_user')
  552. def before_create_page(request):
  553. return HttpResponse("A user creation form", content_type="text/plain")
  554. .. _after_delete_user:
  555. ``after_delete_user``
  556. ~~~~~~~~~~~~~~~~~~~~~
  557. Do something after a ``User`` object is deleted. Uses the same behaviour as ``after_create_user``.
  558. .. _before_delete_user:
  559. ``before_delete_user``
  560. ~~~~~~~~~~~~~~~~~~~~~~
  561. Called at the beginning of the "delete user" view passing in the request and the user object.
  562. Uses the same behaviour as ``before_create_user``.
  563. .. _after_edit_user:
  564. ``after_edit_user``
  565. ~~~~~~~~~~~~~~~~~~~
  566. Do something with a ``User`` object after it has been updated. Uses the same behaviour as ``after_create_user``.
  567. .. _before_edit_user:
  568. ``before_edit_user``
  569. ~~~~~~~~~~~~~~~~~~~~~
  570. Called at the beginning of the "edit user" view passing in the request and the user object.
  571. Uses the same behaviour as ``before_create_user``.
  572. Choosers
  573. --------
  574. .. _construct_page_chooser_queryset:
  575. ``construct_page_chooser_queryset``
  576. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  577. Called when rendering the page chooser view, to allow the page listing QuerySet to be customised. The callable passed into the hook will receive the current page QuerySet and the request object, and must return a Page QuerySet (either the original one, or a new one).
  578. .. code-block:: python
  579. from wagtail.core import hooks
  580. @hooks.register('construct_page_chooser_queryset')
  581. def show_my_pages_only(pages, request):
  582. # Only show own pages
  583. pages = pages.filter(owner=request.user)
  584. return pages
  585. .. _construct_document_chooser_queryset:
  586. ``construct_document_chooser_queryset``
  587. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  588. Called when rendering the document chooser view, to allow the document listing QuerySet to be customised. The callable passed into the hook will receive the current document QuerySet and the request object, and must return a Document QuerySet (either the original one, or a new one).
  589. .. code-block:: python
  590. from wagtail.core import hooks
  591. @hooks.register('construct_document_chooser_queryset')
  592. def show_my_uploaded_documents_only(documents, request):
  593. # Only show uploaded documents
  594. documents = documents.filter(uploaded_by_user=request.user)
  595. return documents
  596. .. _construct_image_chooser_queryset:
  597. ``construct_image_chooser_queryset``
  598. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  599. Called when rendering the image chooser view, to allow the image listing QuerySet to be customised. The callable passed into the hook will receive the current image QuerySet and the request object, and must return an Image QuerySet (either the original one, or a new one).
  600. .. code-block:: python
  601. from wagtail.core import hooks
  602. @hooks.register('construct_image_chooser_queryset')
  603. def show_my_uploaded_images_only(images, request):
  604. # Only show uploaded images
  605. images = images.filter(uploaded_by_user=request.user)
  606. return images
  607. Page explorer
  608. -------------
  609. .. _construct_explorer_page_queryset:
  610. ``construct_explorer_page_queryset``
  611. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  612. Called when rendering the page explorer view, to allow the page listing QuerySet to be customised. The callable passed into the hook will receive the parent page object, the current page QuerySet, and the request object, and must return a Page QuerySet (either the original one, or a new one).
  613. .. code-block:: python
  614. from wagtail.core import hooks
  615. @hooks.register('construct_explorer_page_queryset')
  616. def show_my_profile_only(parent_page, pages, request):
  617. # If we're in the 'user-profiles' section, only show the user's own profile
  618. if parent_page.slug == 'user-profiles':
  619. pages = pages.filter(owner=request.user)
  620. return pages
  621. .. _register_page_listing_buttons:
  622. ``register_page_listing_buttons``
  623. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  624. Add buttons to the actions list for a page in the page explorer. This is useful when adding custom actions to the listing, such as translations or a complex workflow.
  625. This example will add a simple button to the listing:
  626. .. code-block:: python
  627. from wagtail.admin import widgets as wagtailadmin_widgets
  628. @hooks.register('register_page_listing_buttons')
  629. def page_listing_buttons(page, page_perms, is_parent=False, next_url=None):
  630. yield wagtailadmin_widgets.PageListingButton(
  631. 'A page listing button',
  632. '/goes/to/a/url/',
  633. priority=10
  634. )
  635. The arguments passed to the hook are as follows:
  636. * ``page`` - the page object to generate the button for
  637. * ``page_perms`` - a ``PagePermissionTester`` object that can be queried to determine the current user's permissions on the given page
  638. * ``is_parent`` - if true, this button is being rendered for the parent page being displayed at the top of the listing
  639. * ``next_url`` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
  640. The ``priority`` argument controls the order the buttons are displayed in. Buttons are ordered from low to high priority, so a button with ``priority=10`` will be displayed before a button with ``priority=20``.
  641. .. register_page_listing_more_buttons:
  642. ``register_page_listing_more_buttons``
  643. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  644. Add buttons to the "More" dropdown menu for a page in the page explorer. This works similarly to the ``register_page_listing_buttons`` hook but is useful for lesser-used custom actions that are better suited for the dropdown.
  645. This example will add a simple button to the dropdown menu:
  646. .. code-block:: python
  647. from wagtail.admin import widgets as wagtailadmin_widgets
  648. @hooks.register('register_page_listing_more_buttons')
  649. def page_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
  650. yield wagtailadmin_widgets.Button(
  651. 'A dropdown button',
  652. '/goes/to/a/url/',
  653. priority=60
  654. )
  655. The arguments passed to the hook are as follows:
  656. * ``page`` - the page object to generate the button for
  657. * ``page_perms`` - a ``PagePermissionTester`` object that can be queried to determine the current user's permissions on the given page
  658. * ``is_parent`` - if true, this button is being rendered for the parent page being displayed at the top of the listing
  659. * ``next_url`` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
  660. The ``priority`` argument controls the order the buttons are displayed in the dropdown. Buttons are ordered from low to high priority, so a button with ``priority=10`` will be displayed before a button with ``priority=60``.
  661. Buttons with dropdown lists
  662. ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  663. The admin widgets also provide ``ButtonWithDropdownFromHook``, which allows you to define a custom hook for generating a dropdown menu that gets attached to your button.
  664. Creating a button with a dropdown menu involves two steps. Firstly, you add your button to the ``register_page_listing_buttons`` hook, just like the example above.
  665. Secondly, you register a new hook that yields the contents of the dropdown menu.
  666. This example shows how Wagtail's default admin dropdown is implemented. You can also see how to register buttons conditionally, in this case by evaluating the ``page_perms``:
  667. .. code-block:: python
  668. from wagtail.admin import widgets as wagtailadmin_widgets
  669. @hooks.register('register_page_listing_buttons')
  670. def page_custom_listing_buttons(page, page_perms, is_parent=False, next_url=None):
  671. yield wagtailadmin_widgets.ButtonWithDropdownFromHook(
  672. 'More actions',
  673. hook_name='my_button_dropdown_hook',
  674. page=page,
  675. page_perms=page_perms,
  676. is_parent=is_parent,
  677. next_url=next_url,
  678. priority=50
  679. )
  680. @hooks.register('my_button_dropdown_hook')
  681. def page_custom_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
  682. if page_perms.can_move():
  683. yield wagtailadmin_widgets.Button('Move', reverse('wagtailadmin_pages:move', args=[page.id]), priority=10)
  684. if page_perms.can_delete():
  685. yield wagtailadmin_widgets.Button('Delete', reverse('wagtailadmin_pages:delete', args=[page.id]), priority=30)
  686. if page_perms.can_unpublish():
  687. yield wagtailadmin_widgets.Button('Unpublish', reverse('wagtailadmin_pages:unpublish', args=[page.id]), priority=40)
  688. The template for the dropdown button can be customised by overriding ``wagtailadmin/pages/listing/_button_with_dropdown.html``. The JavaScript that runs the dropdowns makes use of custom data attributes, so you should leave ``data-dropdown`` and ``data-dropdown-toggle`` in the markup if you customise it.
  689. Page serving
  690. ------------
  691. .. _before_serve_page:
  692. ``before_serve_page``
  693. ~~~~~~~~~~~~~~~~~~~~~
  694. Called when Wagtail is about to serve a page. The callable passed into the hook will receive the page object, the request object, and the ``args`` and ``kwargs`` that will be passed to the page's ``serve()`` method. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``serve()`` on the page.
  695. .. code-block:: python
  696. from django.http import HttpResponse
  697. from wagtail.core import hooks
  698. @hooks.register('before_serve_page')
  699. def block_googlebot(page, request, serve_args, serve_kwargs):
  700. if request.META.get('HTTP_USER_AGENT') == 'GoogleBot':
  701. return HttpResponse("<h1>bad googlebot no cookie</h1>")
  702. Document serving
  703. ----------------
  704. .. _before_serve_document:
  705. ``before_serve_document``
  706. ~~~~~~~~~~~~~~~~~~~~~~~~~
  707. Called when Wagtail is about to serve a document. The callable passed into the hook will receive the document object and the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, instead of serving the document. Note that this hook will be skipped if the :ref:`WAGTAILDOCS_SERVE_METHOD <wagtaildocs_serve_method>` setting is set to ``direct``.
  708. Snippets
  709. --------
  710. Hooks for working with registered Snippets.
  711. .. _after_edit_snippet:
  712. ``after_edit_snippet``
  713. ~~~~~~~~~~~~~~~~~~~~~~
  714. Called when a Snippet is edited. The callable passed into the hook will receive the model instance, the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``redirect()`` to the listing view.
  715. .. code-block:: python
  716. from django.http import HttpResponse
  717. from wagtail.core import hooks
  718. @hooks.register('after_edit_snippet')
  719. def after_snippet_update(request, instance):
  720. return HttpResponse(f"Congrats on editing a snippet with id {instance.pk}", content_type="text/plain")
  721. .. _before_edit_snippet:
  722. ``before_edit_snippet``
  723. ~~~~~~~~~~~~~~~~~~~~~~~
  724. Called at the beginning of the edit snippet view. The callable passed into the hook will receive the model instance, the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``redirect()`` to the listing view.
  725. .. code-block:: python
  726. from django.http import HttpResponse
  727. from wagtail.core import hooks
  728. @hooks.register('before_edit_snippet')
  729. def block_snippet_edit(request, instance):
  730. if isinstance(instance, RestrictedSnippet) and instance.prevent_edit:
  731. return HttpResponse("Sorry, you can't edit this snippet", content_type="text/plain")
  732. .. _after_create_snippet:
  733. ``after_create_snippet``
  734. ~~~~~~~~~~~~~~~~~~~~~~~~
  735. Called when a Snippet is created. ``after_create_snippet`` and
  736. ``after_edit_snippet`` work in identical ways. The only difference is where
  737. the hook is called.
  738. .. _before_create_snippet:
  739. ``before_create_snippet``
  740. ~~~~~~~~~~~~~~~~~~~~~~~~~
  741. Called at the beginning of the create snippet view. Works in a similar way to `before_edit_snippet` except the model is passed as an argument instead of an instance.
  742. .. _after_delete_snippet:
  743. ``after_delete_snippet``
  744. ~~~~~~~~~~~~~~~~~~~~~~~~
  745. Called when a Snippet is deleted. The callable passed into the hook will receive the model instance(s) as a queryset along with the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``redirect()`` to the listing view.
  746. .. code-block:: python
  747. from django.http import HttpResponse
  748. from wagtail.core import hooks
  749. @hooks.register('after_delete_snippet')
  750. def after_snippet_delete(request, instances):
  751. # "instances" is a QuerySet
  752. total = len(instances)
  753. return HttpResponse(f"{total} snippets have been deleted", content_type="text/plain")
  754. .. _before_delete_snippet:
  755. ``before_delete_snippet``
  756. ~~~~~~~~~~~~~~~~~~~~~~~~~
  757. Called at the beginning of the delete snippet view. The callable passed into the hook will receive the model instance(s) as a queryset along with the request object. If the callable returns an ``HttpResponse``, that response will be returned immediately to the user, and Wagtail will not proceed to call ``redirect()`` to the listing view.
  758. .. code-block:: python
  759. from django.http import HttpResponse
  760. from wagtail.core import hooks
  761. @hooks.register('before_delete_snippet')
  762. def before_snippet_delete(request, instances):
  763. # "instances" is a QuerySet
  764. total = len(instances)
  765. if request.method == 'POST':
  766. # Override the deletion behaviour
  767. instances.delete()
  768. return HttpResponse(f"{total} snippets have been deleted", content_type="text/plain")
  769. .. _register_snippet_action_menu_item:
  770. ``register_snippet_action_menu_item``
  771. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  772. Add an item to the popup menu of actions on the snippet creation and edit views.
  773. The callable passed to this hook must return an instance of
  774. ``wagtail.snippets.action_menu.ActionMenuItem``. ``ActionMenuItem`` is a subclass of :ref:`Component <creating_template_components>` and so the rendering of the menu item can be customised through ``template_name``, ``get_context_data``, ``render_html`` and ``Media``. In addition, the following attributes and methods are available to be overridden:
  775. :order: an integer (default 100) which determines the item's position in the menu. Can also be passed as a keyword argument to the object constructor. The lowest-numbered item in this sequence will be selected as the default menu item; as standard, this is "Save draft" (which has an ``order`` of 0).
  776. :label: the displayed text of the menu item
  777. :get_url: a method which returns a URL for the menu item to link to; by default, returns ``None`` which causes the menu item to behave as a form submit button instead
  778. :name: value of the ``name`` attribute of the submit button if no URL is specified
  779. :icon_name: icon to display against the menu item
  780. :classname: a ``class`` attribute value to add to the button element
  781. :is_shown: a method which returns a boolean indicating whether the menu item should be shown; by default, true except when editing a locked page
  782. The ``get_url``, ``is_shown``, ``get_context_data`` and ``render_html`` methods all accept a context dictionary containing the following fields:
  783. :view: name of the current view: ``'create'`` or ``'edit'``
  784. :model: The snippet's model class
  785. :instance: For ``view`` = ``'edit'``, the instance being edited
  786. :request: The current request object
  787. .. code-block:: python
  788. from wagtail.core import hooks
  789. from wagtail.snippets.action_menu import ActionMenuItem
  790. class GuacamoleMenuItem(ActionMenuItem):
  791. name = 'action-guacamole'
  792. label = "Guacamole"
  793. def get_url(self, context):
  794. return "https://www.youtube.com/watch?v=dNJdJIwCF_Y"
  795. @hooks.register('register_snippet_action_menu_item')
  796. def register_guacamole_menu_item():
  797. return GuacamoleMenuItem(order=10)
  798. .. _construct_snippet_action_menu:
  799. ``construct_snippet_action_menu``
  800. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  801. Modify the final list of action menu items on the snippet creation and edit views.
  802. The callable passed to this hook receives a list of ``ActionMenuItem`` objects, a
  803. request object and a context dictionary as per ``register_snippet_action_menu_item``,
  804. and should modify the list of menu items in-place.
  805. .. code-block:: python
  806. @hooks.register('construct_snippet_action_menu')
  807. def remove_delete_option(menu_items, request, context):
  808. menu_items[:] = [item for item in menu_items if item.name != 'delete']
  809. The ``construct_snippet_action_menu`` hook is called after the menu items have been
  810. sorted by their order attributes, and so setting a menu item's order will have no
  811. effect at this point. Instead, items can be reordered by changing their position in
  812. the list, with the first item being selected as the default action. For example, to
  813. change the default action to Delete:
  814. .. code-block:: python
  815. @hooks.register('construct_snippet_action_menu')
  816. def make_delete_default_action(menu_items, request, context):
  817. for (index, item) in enumerate(menu_items):
  818. if item.name == 'delete':
  819. # move to top of list
  820. menu_items.pop(index)
  821. menu_items.insert(0, item)
  822. break
  823. .. _register_snippet_listing_buttons:
  824. ``register_snippet_listing_buttons``
  825. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  826. Add buttons to the actions list for a snippet in the snippets listing. This is useful when adding custom actions to the listing, such as translations or a complex workflow.
  827. This example will add a simple button to the listing:
  828. .. code-block:: python
  829. from wagtail.snippets import widgets as wagtailsnippets_widgets
  830. @hooks.register('register_snippet_listing_buttons')
  831. def snippet_listing_buttons(snippet, user, next_url=None):
  832. yield wagtailsnippets_widgets.SnippetListingButton(
  833. 'A page listing button',
  834. '/goes/to/a/url/',
  835. priority=10
  836. )
  837. The arguments passed to the hook are as follows:
  838. * ``snippet`` - the snippet object to generate the button for
  839. * ``user`` - the user who is viewing the snippets listing
  840. * ``next_url`` - the URL that the linked action should redirect back to on completion of the action, if the view supports it
  841. The ``priority`` argument controls the order the buttons are displayed in. Buttons are ordered from low to high priority, so a button with ``priority=10`` will be displayed before a button with ``priority=20``.
  842. .. construct_snippet_listing_buttons:
  843. ``construct_snippet_listing_buttons``
  844. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  845. Modify the final list of snippet listing buttons. The
  846. callable passed to this hook receives a list of ``SnippetListingButton`` objects, a user,
  847. and a context dictionary as per ``register_snippet_listing_buttons``,
  848. and should modify the list of menu items in-place.
  849. .. code-block:: python
  850. @hooks.register('construct_snippet_listing_buttons')
  851. def remove_snippet_listing_button_item(buttons, snippet, user, context=None):
  852. buttons.pop() # Removes the 'delete' button
  853. Bulk actions
  854. ------------
  855. Hooks for registering and customising bulk actions. See :ref:`here <custom_bulk_actions>` on how to write custom bulk actions.
  856. .. _register_bulk_action:
  857. ``register_bulk_action``
  858. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  859. Registers a new bulk action to add to the list of bulk actions in the explorer
  860. This hook must be registered with a sub-class of ``BulkAction`` . For example:
  861. .. code-block:: python
  862. from wagtail.admin.views.bulk_action import BulkAction
  863. from wagtail.core import hooks
  864. @hooks.register("register_bulk_action")
  865. class CustomBulkAction(BulkAction):
  866. display_name = _("Custom Action")
  867. action_type = "action"
  868. aria_label = _("Do custom action")
  869. template_name = "/path/to/template"
  870. models = [...] # list of models the action should execute upon
  871. @classmethod
  872. def execute_action(cls, objects, **kwargs):
  873. for object in objects:
  874. do_something(object)
  875. return num_parent_objects, num_child_objects # return the count of updated objects
  876. .. _before_bulk_action:
  877. ``before_bulk_action``
  878. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  879. Do something right before a bulk action is executed (before the ``execute_action`` method is called)
  880. This hook can be used to return an HTTP response. For example:
  881. .. code-block:: python
  882. from wagtail.core import hooks
  883. @hooks.register("before_bulk_action")
  884. def hook_func(request, action_type, objects, action_class_instance):
  885. if action_type == 'delete':
  886. return HttpResponse(f"{len(objects)} objects would be deleted", content_type="text/plain")
  887. .. _after_bulk_action:
  888. ``after_bulk_action``
  889. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  890. Do something right after a bulk action is executed (after the ``execute_action`` method is called)
  891. This hook can be used to return an HTTP response. For example:
  892. .. code-block:: python
  893. from wagtail.core import hooks
  894. @hooks.register("after_bulk_action")
  895. def hook_func(request, action_type, objects, action_class_instance):
  896. if action_type == 'delete':
  897. return HttpResponse(f"{len(objects)} objects have been deleted", content_type="text/plain")
  898. Audit log
  899. ---------
  900. .. _register_log_actions:
  901. ``register_log_actions``
  902. ~~~~~~~~~~~~~~~~~~~~~~~~
  903. See :ref:`audit_log`
  904. To add new actions to the registry, call the ``register_action`` method with the action type, its label and the message to be displayed in administrative listings.
  905. .. code-block:: python
  906. from django.utils.translation import gettext_lazy as _
  907. from wagtail.core import hooks
  908. @hooks.register('register_log_actions')
  909. def additional_log_actions(actions):
  910. actions.register_action('wagtail_package.echo', _('Echo'), _('Sent an echo'))
  911. Alternatively, for a log message that varies according to the log entry's data, create a subclass of ``wagtail.core.log_actions.LogFormatter`` that overrides the ``format_message`` method, and use ``register_action`` as a decorator on that class:
  912. .. code-block:: python
  913. from django.utils.translation import gettext_lazy as _
  914. from wagtail.core import hooks
  915. from wagtail.core.log_actions import LogFormatter
  916. @hooks.register('register_log_actions')
  917. def additional_log_actions(actions):
  918. @actions.register_action('wagtail_package.greet_audience')
  919. class GreetingActionFormatter(LogFormatter):
  920. label = _('Greet audience')
  921. def format_message(self, log_entry):
  922. return _('Hello %(audience)s') % {
  923. 'audience': log_entry.data['audience'],
  924. }
  925. .. versionchanged:: 2.15
  926. The ``LogFormatter`` class was introduced. Previously, dynamic messages were achieved by passing a callable as the ``message`` argument to ``register_action``.