wagtail_hooks.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. from django.conf import settings
  2. from django.contrib.auth.models import Permission
  3. from django.urls import reverse
  4. from django.utils.http import urlencode
  5. from django.utils.translation import gettext
  6. from django.utils.translation import gettext_lazy as _
  7. from draftjs_exporter.dom import DOM
  8. import wagtail.admin.rich_text.editors.draftail.features as draftail_features
  9. from wagtail import __version__, hooks
  10. from wagtail.admin.admin_url_finder import (
  11. ModelAdminURLFinder,
  12. register_admin_url_finder,
  13. )
  14. from wagtail.admin.auth import user_has_any_page_permission
  15. from wagtail.admin.forms.collections import GroupCollectionManagementPermissionFormSet
  16. from wagtail.admin.menu import MenuItem, SubmenuMenuItem, reports_menu, settings_menu
  17. from wagtail.admin.navigation import get_explorable_root_page
  18. from wagtail.admin.rich_text.converters.contentstate import link_entity
  19. from wagtail.admin.rich_text.converters.editor_html import (
  20. LinkTypeRule,
  21. PageLinkHandler,
  22. WhitelistRule,
  23. )
  24. from wagtail.admin.rich_text.converters.html_to_contentstate import (
  25. BlockElementHandler,
  26. ExternalLinkElementHandler,
  27. HorizontalRuleHandler,
  28. InlineStyleElementHandler,
  29. ListElementHandler,
  30. ListItemElementHandler,
  31. PageLinkElementHandler,
  32. )
  33. from wagtail.admin.search import SearchArea
  34. from wagtail.admin.site_summary import PagesSummaryItem
  35. from wagtail.admin.ui.sidebar import (
  36. PageExplorerMenuItem as PageExplorerMenuItemComponent,
  37. )
  38. from wagtail.admin.ui.sidebar import SubMenuItem as SubMenuItemComponent
  39. from wagtail.admin.views.pages.bulk_actions import (
  40. DeleteBulkAction,
  41. MoveBulkAction,
  42. PublishBulkAction,
  43. UnpublishBulkAction,
  44. )
  45. from wagtail.admin.viewsets import viewsets
  46. from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook, PageListingButton
  47. from wagtail.models import Collection, Page, Task, UserPagePermissionsProxy, Workflow
  48. from wagtail.permissions import (
  49. collection_permission_policy,
  50. task_permission_policy,
  51. workflow_permission_policy,
  52. )
  53. from wagtail.whitelist import allow_without_attributes, attribute_rule, check_url
  54. class ExplorerMenuItem(MenuItem):
  55. template = "wagtailadmin/shared/explorer_menu_item.html"
  56. def is_shown(self, request):
  57. return user_has_any_page_permission(request.user)
  58. def get_context(self, request):
  59. context = super().get_context(request)
  60. start_page = get_explorable_root_page(request.user)
  61. if start_page:
  62. context["start_page_id"] = start_page.id
  63. return context
  64. def render_component(self, request):
  65. start_page = get_explorable_root_page(request.user)
  66. if start_page:
  67. return PageExplorerMenuItemComponent(
  68. self.name,
  69. self.label,
  70. self.url,
  71. start_page.id,
  72. icon_name=self.icon_name,
  73. classnames=self.classnames,
  74. )
  75. else:
  76. return super().render_component(request)
  77. @hooks.register("register_admin_menu_item")
  78. def register_explorer_menu_item():
  79. return ExplorerMenuItem(
  80. _("Pages"),
  81. reverse("wagtailadmin_explore_root"),
  82. name="explorer",
  83. icon_name="folder-open-inverse",
  84. order=100,
  85. )
  86. class SettingsMenuItem(SubmenuMenuItem):
  87. template = "wagtailadmin/shared/menu_settings_menu_item.html"
  88. def render_component(self, request):
  89. return SubMenuItemComponent(
  90. self.name,
  91. self.label,
  92. self.menu.render_component(request),
  93. icon_name=self.icon_name,
  94. classnames=self.classnames,
  95. footer_text="Wagtail v." + __version__,
  96. )
  97. @hooks.register("register_admin_menu_item")
  98. def register_settings_menu():
  99. return SettingsMenuItem(_("Settings"), settings_menu, icon_name="cogs", order=10000)
  100. @hooks.register("register_permissions")
  101. def register_permissions():
  102. return Permission.objects.filter(
  103. content_type__app_label="wagtailadmin", codename="access_admin"
  104. )
  105. class PageSearchArea(SearchArea):
  106. def __init__(self):
  107. super().__init__(
  108. _("Pages"),
  109. reverse("wagtailadmin_pages:search"),
  110. name="pages",
  111. icon_name="folder-open-inverse",
  112. order=100,
  113. )
  114. def is_shown(self, request):
  115. return user_has_any_page_permission(request.user)
  116. @hooks.register("register_admin_search_area")
  117. def register_pages_search_area():
  118. return PageSearchArea()
  119. @hooks.register("register_group_permission_panel")
  120. def register_collection_permissions_panel():
  121. return GroupCollectionManagementPermissionFormSet
  122. class CollectionsMenuItem(MenuItem):
  123. def is_shown(self, request):
  124. return collection_permission_policy.user_has_any_permission(
  125. request.user, ["add", "change", "delete"]
  126. )
  127. @hooks.register("register_settings_menu_item")
  128. def register_collections_menu_item():
  129. return CollectionsMenuItem(
  130. _("Collections"),
  131. reverse("wagtailadmin_collections:index"),
  132. icon_name="folder-open-1",
  133. order=700,
  134. )
  135. class WorkflowsMenuItem(MenuItem):
  136. def is_shown(self, request):
  137. if not getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
  138. return False
  139. return workflow_permission_policy.user_has_any_permission(
  140. request.user, ["add", "change", "delete"]
  141. )
  142. class WorkflowTasksMenuItem(MenuItem):
  143. def is_shown(self, request):
  144. if not getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
  145. return False
  146. return task_permission_policy.user_has_any_permission(
  147. request.user, ["add", "change", "delete"]
  148. )
  149. @hooks.register("register_settings_menu_item")
  150. def register_workflows_menu_item():
  151. return WorkflowsMenuItem(
  152. _("Workflows"),
  153. reverse("wagtailadmin_workflows:index"),
  154. icon_name="tasks",
  155. order=100,
  156. )
  157. @hooks.register("register_settings_menu_item")
  158. def register_workflow_tasks_menu_item():
  159. return WorkflowTasksMenuItem(
  160. _("Workflow tasks"),
  161. reverse("wagtailadmin_workflows:task_index"),
  162. icon_name="thumbtack",
  163. order=150,
  164. )
  165. @hooks.register("register_page_listing_buttons")
  166. def page_listing_buttons(page, page_perms, is_parent=False, next_url=None):
  167. if page_perms.can_edit():
  168. yield PageListingButton(
  169. _("Edit"),
  170. reverse("wagtailadmin_pages:edit", args=[page.id]),
  171. attrs={
  172. "aria-label": _("Edit '%(title)s'")
  173. % {"title": page.get_admin_display_title()}
  174. },
  175. priority=10,
  176. )
  177. if page.has_unpublished_changes and page.is_previewable():
  178. yield PageListingButton(
  179. _("View draft"),
  180. reverse("wagtailadmin_pages:view_draft", args=[page.id]),
  181. attrs={
  182. "aria-label": _("Preview draft version of '%(title)s'")
  183. % {"title": page.get_admin_display_title()},
  184. "rel": "noreferrer",
  185. },
  186. priority=20,
  187. )
  188. if page.live and page.url:
  189. yield PageListingButton(
  190. _("View live"),
  191. page.url,
  192. attrs={
  193. "rel": "noreferrer",
  194. "aria-label": _("View live version of '%(title)s'")
  195. % {"title": page.get_admin_display_title()},
  196. },
  197. priority=30,
  198. )
  199. if page_perms.can_add_subpage():
  200. if is_parent:
  201. yield Button(
  202. _("Add child page"),
  203. reverse("wagtailadmin_pages:add_subpage", args=[page.id]),
  204. attrs={
  205. "aria-label": _("Add a child page to '%(title)s' ")
  206. % {"title": page.get_admin_display_title()},
  207. },
  208. classes={
  209. "button",
  210. "button-small",
  211. "bicolor",
  212. "icon",
  213. "white",
  214. "icon-plus",
  215. },
  216. priority=40,
  217. )
  218. else:
  219. yield PageListingButton(
  220. _("Add child page"),
  221. reverse("wagtailadmin_pages:add_subpage", args=[page.id]),
  222. attrs={
  223. "aria-label": _("Add a child page to '%(title)s' ")
  224. % {"title": page.get_admin_display_title()}
  225. },
  226. priority=40,
  227. )
  228. yield ButtonWithDropdownFromHook(
  229. _("More"),
  230. hook_name="register_page_listing_more_buttons",
  231. page=page,
  232. page_perms=page_perms,
  233. is_parent=is_parent,
  234. next_url=next_url,
  235. attrs={
  236. "target": "_blank",
  237. "rel": "noreferrer",
  238. "title": _("View more options for '%(title)s'")
  239. % {"title": page.get_admin_display_title()},
  240. },
  241. priority=50,
  242. )
  243. @hooks.register("register_page_listing_more_buttons")
  244. def page_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
  245. if page_perms.can_move():
  246. yield Button(
  247. _("Move"),
  248. reverse("wagtailadmin_pages:move", args=[page.id]),
  249. attrs={
  250. "title": _("Move page '%(title)s'")
  251. % {"title": page.get_admin_display_title()}
  252. },
  253. priority=10,
  254. )
  255. if page_perms.can_copy():
  256. url = reverse("wagtailadmin_pages:copy", args=[page.id])
  257. if next_url:
  258. url += "?" + urlencode({"next": next_url})
  259. yield Button(
  260. _("Copy"),
  261. url,
  262. attrs={
  263. "title": _("Copy page '%(title)s'")
  264. % {"title": page.get_admin_display_title()}
  265. },
  266. priority=20,
  267. )
  268. if page_perms.can_delete():
  269. url = reverse("wagtailadmin_pages:delete", args=[page.id])
  270. # After deleting the page, it is impossible to redirect to it.
  271. if next_url == reverse("wagtailadmin_explore", args=[page.id]):
  272. next_url = None
  273. if next_url:
  274. url += "?" + urlencode({"next": next_url})
  275. yield Button(
  276. _("Delete"),
  277. url,
  278. attrs={
  279. "title": _("Delete page '%(title)s'")
  280. % {"title": page.get_admin_display_title()}
  281. },
  282. priority=30,
  283. )
  284. if page_perms.can_unpublish():
  285. url = reverse("wagtailadmin_pages:unpublish", args=[page.id])
  286. if next_url:
  287. url += "?" + urlencode({"next": next_url})
  288. yield Button(
  289. _("Unpublish"),
  290. url,
  291. attrs={
  292. "title": _("Unpublish page '%(title)s'")
  293. % {"title": page.get_admin_display_title()}
  294. },
  295. priority=40,
  296. )
  297. if page_perms.can_view_revisions():
  298. yield Button(
  299. _("History"),
  300. reverse("wagtailadmin_pages:history", args=[page.id]),
  301. attrs={
  302. "title": _("View page history for '%(title)s'")
  303. % {"title": page.get_admin_display_title()}
  304. },
  305. priority=50,
  306. )
  307. if is_parent:
  308. yield Button(
  309. _("Sort menu order"),
  310. "?ordering=ord",
  311. attrs={
  312. "title": _("Change ordering of child pages of '%(title)s'")
  313. % {"title": page.get_admin_display_title()}
  314. },
  315. priority=60,
  316. )
  317. @hooks.register("register_page_header_buttons")
  318. def page_header_buttons(page, page_perms, next_url=None):
  319. if page_perms.can_move():
  320. yield Button(
  321. _("Move"),
  322. reverse("wagtailadmin_pages:move", args=[page.id]),
  323. attrs={
  324. "title": _("Move page '%(title)s'")
  325. % {"title": page.get_admin_display_title()}
  326. },
  327. priority=10,
  328. )
  329. if page_perms.can_copy():
  330. url = reverse("wagtailadmin_pages:copy", args=[page.id])
  331. if next_url:
  332. url += "?" + urlencode({"next": next_url})
  333. yield Button(
  334. _("Copy"),
  335. url,
  336. attrs={
  337. "title": _("Copy page '%(title)s'")
  338. % {"title": page.get_admin_display_title()}
  339. },
  340. priority=20,
  341. )
  342. if page_perms.can_add_subpage():
  343. yield Button(
  344. _("Add child page"),
  345. reverse("wagtailadmin_pages:add_subpage", args=[page.id]),
  346. attrs={
  347. "aria-label": _("Add a child page to '%(title)s' ")
  348. % {"title": page.get_admin_display_title()},
  349. },
  350. priority=30,
  351. )
  352. @hooks.register("register_admin_urls")
  353. def register_viewsets_urls():
  354. viewsets.populate()
  355. return viewsets.get_urlpatterns()
  356. @hooks.register("register_rich_text_features")
  357. def register_core_features(features):
  358. features.register_converter_rule(
  359. "editorhtml",
  360. "link",
  361. [
  362. WhitelistRule("a", attribute_rule({"href": check_url})),
  363. LinkTypeRule("page", PageLinkHandler),
  364. ],
  365. )
  366. features.register_converter_rule(
  367. "editorhtml",
  368. "bold",
  369. [
  370. WhitelistRule("b", allow_without_attributes),
  371. WhitelistRule("strong", allow_without_attributes),
  372. ],
  373. )
  374. features.register_converter_rule(
  375. "editorhtml",
  376. "italic",
  377. [
  378. WhitelistRule("i", allow_without_attributes),
  379. WhitelistRule("em", allow_without_attributes),
  380. ],
  381. )
  382. headings_elements = ["h1", "h2", "h3", "h4", "h5", "h6"]
  383. for order, element in enumerate(headings_elements):
  384. features.register_converter_rule(
  385. "editorhtml", element, [WhitelistRule(element, allow_without_attributes)]
  386. )
  387. features.register_converter_rule(
  388. "editorhtml",
  389. "ol",
  390. [
  391. WhitelistRule("ol", allow_without_attributes),
  392. WhitelistRule("li", allow_without_attributes),
  393. ],
  394. )
  395. features.register_converter_rule(
  396. "editorhtml",
  397. "ul",
  398. [
  399. WhitelistRule("ul", allow_without_attributes),
  400. WhitelistRule("li", allow_without_attributes),
  401. ],
  402. )
  403. # Draftail
  404. features.register_editor_plugin(
  405. "draftail", "hr", draftail_features.BooleanFeature("enableHorizontalRule")
  406. )
  407. features.register_converter_rule(
  408. "contentstate",
  409. "hr",
  410. {
  411. "from_database_format": {
  412. "hr": HorizontalRuleHandler(),
  413. },
  414. "to_database_format": {
  415. "entity_decorators": {
  416. "HORIZONTAL_RULE": lambda props: DOM.create_element("hr")
  417. }
  418. },
  419. },
  420. )
  421. features.register_editor_plugin(
  422. "draftail",
  423. "h1",
  424. draftail_features.BlockFeature(
  425. {
  426. "label": "H1",
  427. "type": "header-one",
  428. "description": gettext("Heading %(level)d") % {"level": 1},
  429. }
  430. ),
  431. )
  432. features.register_converter_rule(
  433. "contentstate",
  434. "h1",
  435. {
  436. "from_database_format": {
  437. "h1": BlockElementHandler("header-one"),
  438. },
  439. "to_database_format": {"block_map": {"header-one": "h1"}},
  440. },
  441. )
  442. features.register_editor_plugin(
  443. "draftail",
  444. "h2",
  445. draftail_features.BlockFeature(
  446. {
  447. "label": "H2",
  448. "type": "header-two",
  449. "description": gettext("Heading %(level)d") % {"level": 2},
  450. }
  451. ),
  452. )
  453. features.register_converter_rule(
  454. "contentstate",
  455. "h2",
  456. {
  457. "from_database_format": {
  458. "h2": BlockElementHandler("header-two"),
  459. },
  460. "to_database_format": {"block_map": {"header-two": "h2"}},
  461. },
  462. )
  463. features.register_editor_plugin(
  464. "draftail",
  465. "h3",
  466. draftail_features.BlockFeature(
  467. {
  468. "label": "H3",
  469. "type": "header-three",
  470. "description": gettext("Heading %(level)d") % {"level": 3},
  471. }
  472. ),
  473. )
  474. features.register_converter_rule(
  475. "contentstate",
  476. "h3",
  477. {
  478. "from_database_format": {
  479. "h3": BlockElementHandler("header-three"),
  480. },
  481. "to_database_format": {"block_map": {"header-three": "h3"}},
  482. },
  483. )
  484. features.register_editor_plugin(
  485. "draftail",
  486. "h4",
  487. draftail_features.BlockFeature(
  488. {
  489. "label": "H4",
  490. "type": "header-four",
  491. "description": gettext("Heading %(level)d") % {"level": 4},
  492. }
  493. ),
  494. )
  495. features.register_converter_rule(
  496. "contentstate",
  497. "h4",
  498. {
  499. "from_database_format": {
  500. "h4": BlockElementHandler("header-four"),
  501. },
  502. "to_database_format": {"block_map": {"header-four": "h4"}},
  503. },
  504. )
  505. features.register_editor_plugin(
  506. "draftail",
  507. "h5",
  508. draftail_features.BlockFeature(
  509. {
  510. "label": "H5",
  511. "type": "header-five",
  512. "description": gettext("Heading %(level)d") % {"level": 5},
  513. }
  514. ),
  515. )
  516. features.register_converter_rule(
  517. "contentstate",
  518. "h5",
  519. {
  520. "from_database_format": {
  521. "h5": BlockElementHandler("header-five"),
  522. },
  523. "to_database_format": {"block_map": {"header-five": "h5"}},
  524. },
  525. )
  526. features.register_editor_plugin(
  527. "draftail",
  528. "h6",
  529. draftail_features.BlockFeature(
  530. {
  531. "label": "H6",
  532. "type": "header-six",
  533. "description": gettext("Heading %(level)d") % {"level": 6},
  534. }
  535. ),
  536. )
  537. features.register_converter_rule(
  538. "contentstate",
  539. "h6",
  540. {
  541. "from_database_format": {
  542. "h6": BlockElementHandler("header-six"),
  543. },
  544. "to_database_format": {"block_map": {"header-six": "h6"}},
  545. },
  546. )
  547. features.register_editor_plugin(
  548. "draftail",
  549. "ul",
  550. draftail_features.BlockFeature(
  551. {
  552. "type": "unordered-list-item",
  553. "icon": "list-ul",
  554. "description": gettext("Bulleted list"),
  555. }
  556. ),
  557. )
  558. features.register_converter_rule(
  559. "contentstate",
  560. "ul",
  561. {
  562. "from_database_format": {
  563. "ul": ListElementHandler("unordered-list-item"),
  564. "li": ListItemElementHandler(),
  565. },
  566. "to_database_format": {
  567. "block_map": {"unordered-list-item": {"element": "li", "wrapper": "ul"}}
  568. },
  569. },
  570. )
  571. features.register_editor_plugin(
  572. "draftail",
  573. "ol",
  574. draftail_features.BlockFeature(
  575. {
  576. "type": "ordered-list-item",
  577. "icon": "list-ol",
  578. "description": gettext("Numbered list"),
  579. }
  580. ),
  581. )
  582. features.register_converter_rule(
  583. "contentstate",
  584. "ol",
  585. {
  586. "from_database_format": {
  587. "ol": ListElementHandler("ordered-list-item"),
  588. "li": ListItemElementHandler(),
  589. },
  590. "to_database_format": {
  591. "block_map": {"ordered-list-item": {"element": "li", "wrapper": "ol"}}
  592. },
  593. },
  594. )
  595. features.register_editor_plugin(
  596. "draftail",
  597. "blockquote",
  598. draftail_features.BlockFeature(
  599. {
  600. "type": "blockquote",
  601. "icon": "openquote",
  602. "description": gettext("Blockquote"),
  603. }
  604. ),
  605. )
  606. features.register_converter_rule(
  607. "contentstate",
  608. "blockquote",
  609. {
  610. "from_database_format": {
  611. "blockquote": BlockElementHandler("blockquote"),
  612. },
  613. "to_database_format": {"block_map": {"blockquote": "blockquote"}},
  614. },
  615. )
  616. features.register_editor_plugin(
  617. "draftail",
  618. "bold",
  619. draftail_features.InlineStyleFeature(
  620. {
  621. "type": "BOLD",
  622. "icon": "bold",
  623. "description": gettext("Bold"),
  624. }
  625. ),
  626. )
  627. features.register_converter_rule(
  628. "contentstate",
  629. "bold",
  630. {
  631. "from_database_format": {
  632. "b": InlineStyleElementHandler("BOLD"),
  633. "strong": InlineStyleElementHandler("BOLD"),
  634. },
  635. "to_database_format": {"style_map": {"BOLD": "b"}},
  636. },
  637. )
  638. features.register_editor_plugin(
  639. "draftail",
  640. "italic",
  641. draftail_features.InlineStyleFeature(
  642. {
  643. "type": "ITALIC",
  644. "icon": "italic",
  645. "description": gettext("Italic"),
  646. }
  647. ),
  648. )
  649. features.register_converter_rule(
  650. "contentstate",
  651. "italic",
  652. {
  653. "from_database_format": {
  654. "i": InlineStyleElementHandler("ITALIC"),
  655. "em": InlineStyleElementHandler("ITALIC"),
  656. },
  657. "to_database_format": {"style_map": {"ITALIC": "i"}},
  658. },
  659. )
  660. features.register_editor_plugin(
  661. "draftail",
  662. "link",
  663. draftail_features.EntityFeature(
  664. {
  665. "type": "LINK",
  666. "icon": "link",
  667. "description": gettext("Link"),
  668. # We want to enforce constraints on which links can be pasted into rich text.
  669. # Keep only the attributes Wagtail needs.
  670. "attributes": ["url", "id", "parentId"],
  671. "whitelist": {
  672. # Keep pasted links with http/https protocol, and not-pasted links (href = undefined).
  673. "href": "^(http:|https:|undefined$)",
  674. },
  675. },
  676. js=[
  677. "wagtailadmin/js/page-chooser-modal.js",
  678. ],
  679. ),
  680. )
  681. features.register_converter_rule(
  682. "contentstate",
  683. "link",
  684. {
  685. "from_database_format": {
  686. "a[href]": ExternalLinkElementHandler("LINK"),
  687. 'a[linktype="page"]': PageLinkElementHandler("LINK"),
  688. },
  689. "to_database_format": {"entity_decorators": {"LINK": link_entity}},
  690. },
  691. )
  692. features.register_editor_plugin(
  693. "draftail",
  694. "superscript",
  695. draftail_features.InlineStyleFeature(
  696. {
  697. "type": "SUPERSCRIPT",
  698. "icon": "superscript",
  699. "description": gettext("Superscript"),
  700. }
  701. ),
  702. )
  703. features.register_converter_rule(
  704. "contentstate",
  705. "superscript",
  706. {
  707. "from_database_format": {
  708. "sup": InlineStyleElementHandler("SUPERSCRIPT"),
  709. },
  710. "to_database_format": {"style_map": {"SUPERSCRIPT": "sup"}},
  711. },
  712. )
  713. features.register_editor_plugin(
  714. "draftail",
  715. "subscript",
  716. draftail_features.InlineStyleFeature(
  717. {
  718. "type": "SUBSCRIPT",
  719. "icon": "subscript",
  720. "description": gettext("Subscript"),
  721. }
  722. ),
  723. )
  724. features.register_converter_rule(
  725. "contentstate",
  726. "subscript",
  727. {
  728. "from_database_format": {
  729. "sub": InlineStyleElementHandler("SUBSCRIPT"),
  730. },
  731. "to_database_format": {"style_map": {"SUBSCRIPT": "sub"}},
  732. },
  733. )
  734. features.register_editor_plugin(
  735. "draftail",
  736. "strikethrough",
  737. draftail_features.InlineStyleFeature(
  738. {
  739. "type": "STRIKETHROUGH",
  740. "icon": "strikethrough",
  741. "description": gettext("Strikethrough"),
  742. }
  743. ),
  744. )
  745. features.register_converter_rule(
  746. "contentstate",
  747. "strikethrough",
  748. {
  749. "from_database_format": {
  750. "s": InlineStyleElementHandler("STRIKETHROUGH"),
  751. },
  752. "to_database_format": {"style_map": {"STRIKETHROUGH": "s"}},
  753. },
  754. )
  755. features.register_editor_plugin(
  756. "draftail",
  757. "code",
  758. draftail_features.InlineStyleFeature(
  759. {
  760. "type": "CODE",
  761. "icon": "code",
  762. "description": gettext("Code"),
  763. }
  764. ),
  765. )
  766. features.register_converter_rule(
  767. "contentstate",
  768. "code",
  769. {
  770. "from_database_format": {
  771. "code": InlineStyleElementHandler("CODE"),
  772. },
  773. "to_database_format": {"style_map": {"CODE": "code"}},
  774. },
  775. )
  776. class ReportsMenuItem(SubmenuMenuItem):
  777. template = "wagtailadmin/shared/menu_submenu_item.html"
  778. class LockedPagesMenuItem(MenuItem):
  779. def is_shown(self, request):
  780. return UserPagePermissionsProxy(request.user).can_remove_locks()
  781. class WorkflowReportMenuItem(MenuItem):
  782. def is_shown(self, request):
  783. return getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True)
  784. class SiteHistoryReportMenuItem(MenuItem):
  785. def is_shown(self, request):
  786. return UserPagePermissionsProxy(request.user).explorable_pages().exists()
  787. class AgingPagesReportMenuItem(MenuItem):
  788. def is_shown(self, request):
  789. return getattr(settings, "WAGTAIL_AGING_PAGES_ENABLED", True)
  790. @hooks.register("register_reports_menu_item")
  791. def register_locked_pages_menu_item():
  792. return LockedPagesMenuItem(
  793. _("Locked Pages"),
  794. reverse("wagtailadmin_reports:locked_pages"),
  795. icon_name="lock",
  796. order=700,
  797. )
  798. @hooks.register("register_reports_menu_item")
  799. def register_workflow_report_menu_item():
  800. return WorkflowReportMenuItem(
  801. _("Workflows"),
  802. reverse("wagtailadmin_reports:workflow"),
  803. icon_name="tasks",
  804. order=800,
  805. )
  806. @hooks.register("register_reports_menu_item")
  807. def register_workflow_tasks_report_menu_item():
  808. return WorkflowReportMenuItem(
  809. _("Workflow tasks"),
  810. reverse("wagtailadmin_reports:workflow_tasks"),
  811. icon_name="thumbtack",
  812. order=900,
  813. )
  814. @hooks.register("register_reports_menu_item")
  815. def register_site_history_report_menu_item():
  816. return SiteHistoryReportMenuItem(
  817. _("Site history"),
  818. reverse("wagtailadmin_reports:site_history"),
  819. icon_name="history",
  820. order=1000,
  821. )
  822. @hooks.register("register_reports_menu_item")
  823. def register_aging_pages_report_menu_item():
  824. return AgingPagesReportMenuItem(
  825. _("Aging pages"),
  826. reverse("wagtailadmin_reports:aging_pages"),
  827. icon_name="time",
  828. order=1100,
  829. )
  830. @hooks.register("register_admin_menu_item")
  831. def register_reports_menu():
  832. return ReportsMenuItem(_("Reports"), reports_menu, icon_name="site", order=9000)
  833. @hooks.register("register_icons")
  834. def register_icons(icons):
  835. for icon in [
  836. "angle-double-left.svg",
  837. "angle-double-right.svg",
  838. "arrow-down-big.svg",
  839. "arrow-down.svg",
  840. "arrow-left.svg",
  841. "arrow-right.svg",
  842. "arrow-up-big.svg",
  843. "arrow-up.svg",
  844. "arrows-up-down.svg",
  845. "bars.svg",
  846. "bin.svg",
  847. "bold.svg",
  848. "chain-broken.svg",
  849. "check.svg",
  850. "chevron-down.svg",
  851. "clipboard-list.svg",
  852. "code.svg",
  853. "cog.svg",
  854. "cogs.svg",
  855. "collapse-down.svg",
  856. "collapse-up.svg",
  857. "comment.svg",
  858. "comment-add.svg",
  859. "comment-add-reversed.svg",
  860. "comment-large.svg",
  861. "comment-large-outline.svg",
  862. "comment-large-reversed.svg",
  863. "cross.svg",
  864. "date.svg",
  865. "doc-empty-inverse.svg",
  866. "doc-empty.svg",
  867. "doc-full-inverse.svg",
  868. "doc-full.svg", # aka file-text-alt
  869. "download-alt.svg",
  870. "download.svg",
  871. "draft.svg",
  872. "duplicate.svg",
  873. "edit.svg",
  874. "ellipsis-v.svg",
  875. "expand-right.svg",
  876. "error.svg",
  877. "folder-inverse.svg",
  878. "folder-open-1.svg",
  879. "folder-open-inverse.svg",
  880. "folder.svg",
  881. "form.svg",
  882. "grip.svg",
  883. "group.svg",
  884. "help.svg",
  885. "history.svg",
  886. "home.svg",
  887. "horizontalrule.svg",
  888. "image.svg", # aka picture
  889. "info-circle.svg",
  890. "italic.svg",
  891. "link.svg",
  892. "link-external.svg",
  893. "list-ol.svg",
  894. "list-ul.svg",
  895. "lock-open.svg",
  896. "lock.svg",
  897. "login.svg",
  898. "logout.svg",
  899. "mail.svg",
  900. "media.svg",
  901. "no-view.svg",
  902. "openquote.svg",
  903. "order-down.svg",
  904. "order-up.svg",
  905. "order.svg",
  906. "password.svg",
  907. "pick.svg",
  908. "pilcrow.svg",
  909. "placeholder.svg", # aka marquee
  910. "plus-inverse.svg",
  911. "plus.svg",
  912. "radio-empty.svg",
  913. "radio-full.svg",
  914. "redirect.svg",
  915. "repeat.svg",
  916. "reset.svg",
  917. "resubmit.svg",
  918. "search.svg",
  919. "site.svg",
  920. "snippet.svg",
  921. "spinner.svg",
  922. "strikethrough.svg",
  923. "success.svg",
  924. "subscript.svg",
  925. "superscript.svg",
  926. "table.svg",
  927. "tag.svg",
  928. "tasks.svg",
  929. "thumbtack.svg",
  930. "tick-inverse.svg",
  931. "tick.svg",
  932. "time.svg",
  933. "title.svg",
  934. "undo.svg",
  935. "uni52.svg", # Is this a redundant icon?
  936. "upload.svg",
  937. "user.svg",
  938. "view.svg",
  939. "wagtail-inverse.svg",
  940. "wagtail.svg",
  941. "warning.svg",
  942. ]:
  943. icons.append("wagtailadmin/icons/{}".format(icon))
  944. return icons
  945. @hooks.register("construct_homepage_summary_items")
  946. def add_pages_summary_item(request, items):
  947. items.insert(0, PagesSummaryItem(request))
  948. class PageAdminURLFinder:
  949. def __init__(self, user):
  950. self.page_perms = user and UserPagePermissionsProxy(user)
  951. def get_edit_url(self, instance):
  952. if self.page_perms and not self.page_perms.for_page(instance).can_edit():
  953. return None
  954. else:
  955. return reverse("wagtailadmin_pages:edit", args=(instance.pk,))
  956. register_admin_url_finder(Page, PageAdminURLFinder)
  957. class CollectionAdminURLFinder(ModelAdminURLFinder):
  958. permission_policy = collection_permission_policy
  959. edit_url_name = "wagtailadmin_collections:edit"
  960. register_admin_url_finder(Collection, CollectionAdminURLFinder)
  961. class WorkflowAdminURLFinder(ModelAdminURLFinder):
  962. permission_policy = workflow_permission_policy
  963. edit_url_name = "wagtailadmin_workflows:edit"
  964. register_admin_url_finder(Workflow, WorkflowAdminURLFinder)
  965. class WorkflowTaskAdminURLFinder(ModelAdminURLFinder):
  966. permission_policy = task_permission_policy
  967. edit_url_name = "wagtailadmin_workflows:edit_task"
  968. register_admin_url_finder(Task, WorkflowTaskAdminURLFinder)
  969. for action_class in [
  970. DeleteBulkAction,
  971. MoveBulkAction,
  972. PublishBulkAction,
  973. UnpublishBulkAction,
  974. ]:
  975. hooks.register("register_bulk_action", action_class)