wagtail_hooks.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. from django.conf import settings
  2. from django.contrib.auth.models import Permission
  3. from django.contrib.auth.views import redirect_to_login
  4. from django.db import models
  5. from django.urls import reverse
  6. from django.utils.text import capfirst
  7. from django.utils.translation import gettext_lazy as _
  8. from django.utils.translation import ngettext
  9. from wagtail import hooks
  10. from wagtail.coreutils import get_content_languages
  11. from wagtail.log_actions import LogFormatter
  12. from wagtail.models import ModelLogEntry, Page, PageLogEntry, PageViewRestriction
  13. from wagtail.rich_text.pages import PageLinkHandler
  14. from wagtail.utils.timestamps import parse_datetime_localized, render_timestamp
  15. def require_wagtail_login(next):
  16. login_url = getattr(
  17. settings, "WAGTAIL_FRONTEND_LOGIN_URL", reverse("wagtailcore_login")
  18. )
  19. return redirect_to_login(next, login_url)
  20. @hooks.register("before_serve_page")
  21. def check_view_restrictions(page, request, serve_args, serve_kwargs):
  22. """
  23. Check whether there are any view restrictions on this page which are
  24. not fulfilled by the given request object. If there are, return an
  25. HttpResponse that will notify the user of that restriction (and possibly
  26. include a password / login form that will allow them to proceed). If
  27. there are no such restrictions, return None
  28. """
  29. for restriction in page.get_view_restrictions():
  30. if not restriction.accept_request(request):
  31. if restriction.restriction_type == PageViewRestriction.PASSWORD:
  32. from wagtail.forms import PasswordViewRestrictionForm
  33. form = PasswordViewRestrictionForm(
  34. instance=restriction,
  35. initial={"return_url": request.get_full_path()},
  36. )
  37. action_url = reverse(
  38. "wagtailcore_authenticate_with_password",
  39. args=[restriction.id, page.id],
  40. )
  41. return page.serve_password_required_response(request, form, action_url)
  42. elif restriction.restriction_type in [
  43. PageViewRestriction.LOGIN,
  44. PageViewRestriction.GROUPS,
  45. ]:
  46. return require_wagtail_login(next=request.get_full_path())
  47. @hooks.register("register_rich_text_features")
  48. def register_core_features(features):
  49. features.default_features.append("hr")
  50. features.default_features.append("link")
  51. features.register_link_type(PageLinkHandler)
  52. features.default_features.append("bold")
  53. features.default_features.append("italic")
  54. features.default_features.extend(["h2", "h3", "h4"])
  55. features.default_features.append("ol")
  56. features.default_features.append("ul")
  57. if getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
  58. @hooks.register("register_permissions")
  59. def register_workflow_permissions():
  60. return Permission.objects.filter(
  61. content_type__app_label="wagtailcore",
  62. codename__in=["add_workflow", "change_workflow", "delete_workflow"],
  63. )
  64. @hooks.register("register_permissions")
  65. def register_task_permissions():
  66. return Permission.objects.filter(
  67. content_type__app_label="wagtailcore",
  68. codename__in=["add_task", "change_task", "delete_task"],
  69. )
  70. @hooks.register("describe_collection_contents")
  71. def describe_collection_children(collection):
  72. descendant_count = collection.get_descendants().count()
  73. if descendant_count:
  74. url = reverse("wagtailadmin_collections:index")
  75. return {
  76. "count": descendant_count,
  77. "count_text": ngettext(
  78. "%(count)s descendant collection",
  79. "%(count)s descendant collections",
  80. descendant_count,
  81. )
  82. % {"count": descendant_count},
  83. "url": url,
  84. }
  85. @hooks.register("register_log_actions")
  86. def register_core_log_actions(actions):
  87. actions.register_model(models.Model, ModelLogEntry)
  88. actions.register_model(Page, PageLogEntry)
  89. actions.register_action("wagtail.create", _("Create"), _("Created"))
  90. actions.register_action("wagtail.edit", _("Edit"), _("Edited"))
  91. actions.register_action("wagtail.delete", _("Delete"), _("Deleted"))
  92. actions.register_action("wagtail.publish", _("Publish"), _("Published"))
  93. actions.register_action(
  94. "wagtail.publish.scheduled",
  95. _("Publish scheduled draft"),
  96. _("Published scheduled draft"),
  97. )
  98. actions.register_action("wagtail.unpublish", _("Unpublish"), _("Unpublished"))
  99. actions.register_action(
  100. "wagtail.unpublish.scheduled",
  101. _("Unpublish scheduled draft"),
  102. _("Unpublished scheduled draft"),
  103. )
  104. actions.register_action("wagtail.lock", _("Lock"), _("Locked"))
  105. actions.register_action("wagtail.unlock", _("Unlock"), _("Unlocked"))
  106. # Legacy moderation actions
  107. actions.register_action("wagtail.moderation.approve", _("Approve"), _("Approved"))
  108. actions.register_action("wagtail.moderation.reject", _("Reject"), _("Rejected"))
  109. @actions.register_action("wagtail.rename")
  110. class RenameActionFormatter(LogFormatter):
  111. label = _("Rename")
  112. def format_message(self, log_entry):
  113. try:
  114. return _("Renamed from '%(old)s' to '%(new)s'") % {
  115. "old": log_entry.data["title"]["old"],
  116. "new": log_entry.data["title"]["new"],
  117. }
  118. except KeyError:
  119. return _("Renamed")
  120. @actions.register_action("wagtail.revert")
  121. class RevertActionFormatter(LogFormatter):
  122. label = _("Revert")
  123. def format_message(self, log_entry):
  124. try:
  125. return _(
  126. "Reverted to previous revision with id %(revision_id)s from %(created_at)s"
  127. ) % {
  128. "revision_id": log_entry.data["revision"]["id"],
  129. "created_at": render_timestamp(
  130. parse_datetime_localized(
  131. log_entry.data["revision"]["created"],
  132. )
  133. ),
  134. }
  135. except KeyError:
  136. return _("Reverted to previous revision")
  137. @actions.register_action("wagtail.copy")
  138. class CopyActionFormatter(LogFormatter):
  139. label = _("Copy")
  140. def format_message(self, log_entry):
  141. try:
  142. return _("Copied from %(title)s") % {
  143. "title": log_entry.data["source"]["title"],
  144. }
  145. except KeyError:
  146. return _("Copied")
  147. @actions.register_action("wagtail.copy_for_translation")
  148. class CopyForTranslationActionFormatter(LogFormatter):
  149. label = _("Copy for translation")
  150. def format_message(self, log_entry):
  151. try:
  152. return _("Copied for translation from %(title)s (%(locale)s)") % {
  153. "title": log_entry.data["source"]["title"],
  154. "locale": get_content_languages().get(
  155. log_entry.data["source_locale"]["language_code"]
  156. )
  157. or "",
  158. }
  159. except KeyError:
  160. return _("Copied for translation")
  161. @actions.register_action("wagtail.create_alias")
  162. class CreateAliasActionFormatter(LogFormatter):
  163. label = _("Create alias")
  164. def format_message(self, log_entry):
  165. try:
  166. return _("Created an alias of %(title)s") % {
  167. "title": log_entry.data["source"]["title"],
  168. }
  169. except KeyError:
  170. return _("Created an alias")
  171. @actions.register_action("wagtail.convert_alias")
  172. class ConvertAliasActionFormatter(LogFormatter):
  173. label = _("Convert alias into ordinary page")
  174. def format_message(self, log_entry):
  175. try:
  176. return _("Converted the alias '%(title)s' into an ordinary page") % {
  177. "title": log_entry.data["page"]["title"],
  178. }
  179. except KeyError:
  180. return _("Converted an alias into an ordinary page")
  181. @actions.register_action("wagtail.move")
  182. class MoveActionFormatter(LogFormatter):
  183. label = _("Move")
  184. def format_message(self, log_entry):
  185. try:
  186. return _("Moved from '%(old_parent)s' to '%(new_parent)s'") % {
  187. "old_parent": log_entry.data["source"]["title"],
  188. "new_parent": log_entry.data["destination"]["title"],
  189. }
  190. except KeyError:
  191. return _("Moved")
  192. @actions.register_action("wagtail.reorder")
  193. class ReorderActionFormatter(LogFormatter):
  194. label = _("Reorder")
  195. def format_message(self, log_entry):
  196. try:
  197. return _("Reordered under '%(parent)s'") % {
  198. "parent": log_entry.data["destination"]["title"],
  199. }
  200. except KeyError:
  201. return _("Reordered")
  202. @actions.register_action("wagtail.publish.schedule")
  203. class SchedulePublishActionFormatter(LogFormatter):
  204. label = _("Schedule publication")
  205. def format_message(self, log_entry):
  206. try:
  207. if log_entry.data["revision"]["has_live_version"]:
  208. return _(
  209. "Revision %(revision_id)s from %(created_at)s scheduled for publishing at %(go_live_at)s."
  210. ) % {
  211. "revision_id": log_entry.data["revision"]["id"],
  212. "created_at": render_timestamp(
  213. parse_datetime_localized(
  214. log_entry.data["revision"]["created"],
  215. )
  216. ),
  217. "go_live_at": render_timestamp(
  218. parse_datetime_localized(
  219. log_entry.data["revision"]["go_live_at"],
  220. )
  221. ),
  222. }
  223. else:
  224. return _("Page scheduled for publishing at %(go_live_at)s") % {
  225. "go_live_at": render_timestamp(
  226. parse_datetime_localized(
  227. log_entry.data["revision"]["go_live_at"],
  228. )
  229. ),
  230. }
  231. except KeyError:
  232. return _("Page scheduled for publishing")
  233. @actions.register_action("wagtail.schedule.cancel")
  234. class UnschedulePublicationActionFormatter(LogFormatter):
  235. label = _("Unschedule publication")
  236. def format_message(self, log_entry):
  237. try:
  238. if log_entry.data["revision"]["has_live_version"]:
  239. return _(
  240. "Revision %(revision_id)s from %(created_at)s unscheduled from publishing at %(go_live_at)s."
  241. ) % {
  242. "revision_id": log_entry.data["revision"]["id"],
  243. "created_at": render_timestamp(
  244. parse_datetime_localized(
  245. log_entry.data["revision"]["created"],
  246. )
  247. ),
  248. "go_live_at": render_timestamp(
  249. parse_datetime_localized(
  250. log_entry.data["revision"]["go_live_at"],
  251. )
  252. )
  253. if log_entry.data["revision"]["go_live_at"]
  254. else None,
  255. }
  256. else:
  257. return _("Page unscheduled for publishing at %(go_live_at)s") % {
  258. "go_live_at": render_timestamp(
  259. parse_datetime_localized(
  260. log_entry.data["revision"]["go_live_at"],
  261. )
  262. )
  263. if log_entry.data["revision"]["go_live_at"]
  264. else None,
  265. }
  266. except KeyError:
  267. return _("Page unscheduled from publishing")
  268. @actions.register_action("wagtail.view_restriction.create")
  269. class AddViewRestrictionActionFormatter(LogFormatter):
  270. label = _("Add view restrictions")
  271. def format_message(self, log_entry):
  272. try:
  273. return _("Added the '%(restriction)s' view restriction") % {
  274. "restriction": log_entry.data["restriction"]["title"],
  275. }
  276. except KeyError:
  277. return _("Added view restriction")
  278. @actions.register_action("wagtail.view_restriction.edit")
  279. class EditViewRestrictionActionFormatter(LogFormatter):
  280. label = _("Update view restrictions")
  281. def format_message(self, log_entry):
  282. try:
  283. return _("Updated the view restriction to '%(restriction)s'") % {
  284. "restriction": log_entry.data["restriction"]["title"],
  285. }
  286. except KeyError:
  287. return _("Updated view restriction")
  288. @actions.register_action("wagtail.view_restriction.delete")
  289. class DeleteViewRestrictionActionFormatter(LogFormatter):
  290. label = _("Remove view restrictions")
  291. def format_message(self, log_entry):
  292. try:
  293. return _(
  294. "Removed the '%(restriction)s' view restriction. The page is public."
  295. ) % {
  296. "restriction": log_entry.data["restriction"]["title"],
  297. }
  298. except KeyError:
  299. return _("Removed view restriction")
  300. class CommentLogFormatter(LogFormatter):
  301. @staticmethod
  302. def _field_label_from_content_path(model, content_path):
  303. """
  304. Finds the translated field label for the given model and content path
  305. Raises LookupError if not found
  306. """
  307. field_name = content_path.split(".")[0]
  308. return capfirst(model._meta.get_field(field_name).verbose_name)
  309. @actions.register_action("wagtail.comments.create")
  310. class CreateCommentActionFormatter(CommentLogFormatter):
  311. label = _("Add comment")
  312. def format_message(self, log_entry):
  313. try:
  314. return _('Added a comment on field %(field)s: "%(text)s"') % {
  315. "field": self._field_label_from_content_path(
  316. log_entry.page.specific_class,
  317. log_entry.data["comment"]["contentpath"],
  318. ),
  319. "text": log_entry.data["comment"]["text"],
  320. }
  321. except KeyError:
  322. return _("Added a comment")
  323. @actions.register_action("wagtail.comments.edit")
  324. class EditCommentActionFormatter(CommentLogFormatter):
  325. label = _("Edit comment")
  326. def format_message(self, log_entry):
  327. try:
  328. return _('Edited a comment on field %(field)s: "%(text)s"') % {
  329. "field": self._field_label_from_content_path(
  330. log_entry.page.specific_class,
  331. log_entry.data["comment"]["contentpath"],
  332. ),
  333. "text": log_entry.data["comment"]["text"],
  334. }
  335. except KeyError:
  336. return _("Edited a comment")
  337. @actions.register_action("wagtail.comments.delete")
  338. class DeleteCommentActionFormatter(CommentLogFormatter):
  339. label = _("Delete comment")
  340. def format_message(self, log_entry):
  341. try:
  342. return _('Deleted a comment on field %(field)s: "%(text)s"') % {
  343. "field": self._field_label_from_content_path(
  344. log_entry.page.specific_class,
  345. log_entry.data["comment"]["contentpath"],
  346. ),
  347. "text": log_entry.data["comment"]["text"],
  348. }
  349. except KeyError:
  350. return _("Deleted a comment")
  351. @actions.register_action("wagtail.comments.resolve")
  352. class ResolveCommentActionFormatter(CommentLogFormatter):
  353. label = _("Resolve comment")
  354. def format_message(self, log_entry):
  355. try:
  356. return _('Resolved a comment on field %(field)s: "%(text)s"') % {
  357. "field": self._field_label_from_content_path(
  358. log_entry.page.specific_class,
  359. log_entry.data["comment"]["contentpath"],
  360. ),
  361. "text": log_entry.data["comment"]["text"],
  362. }
  363. except KeyError:
  364. return _("Resolved a comment")
  365. @actions.register_action("wagtail.comments.create_reply")
  366. class CreateReplyActionFormatter(CommentLogFormatter):
  367. label = _("Reply to comment")
  368. def format_message(self, log_entry):
  369. try:
  370. return _('Replied to comment on field %(field)s: "%(text)s"') % {
  371. "field": self._field_label_from_content_path(
  372. log_entry.page.specific_class,
  373. log_entry.data["comment"]["contentpath"],
  374. ),
  375. "text": log_entry.data["reply"]["text"],
  376. }
  377. except KeyError:
  378. return _("Replied to a comment")
  379. @actions.register_action("wagtail.comments.edit_reply")
  380. class EditReplyActionFormatter(CommentLogFormatter):
  381. label = _("Edit reply to comment")
  382. def format_message(self, log_entry):
  383. try:
  384. return _(
  385. 'Edited a reply to a comment on field %(field)s: "%(text)s"'
  386. ) % {
  387. "field": self._field_label_from_content_path(
  388. log_entry.page.specific_class,
  389. log_entry.data["comment"]["contentpath"],
  390. ),
  391. "text": log_entry.data["reply"]["text"],
  392. }
  393. except KeyError:
  394. return _("Edited a reply")
  395. @actions.register_action("wagtail.comments.delete_reply")
  396. class DeleteReplyActionFormatter(CommentLogFormatter):
  397. label = _("Delete reply to comment")
  398. def format_message(self, log_entry):
  399. try:
  400. return _(
  401. 'Deleted a reply to a comment on field %(field)s: "%(text)s"'
  402. ) % {
  403. "field": self._field_label_from_content_path(
  404. log_entry.page.specific_class,
  405. log_entry.data["comment"]["contentpath"],
  406. ),
  407. "text": log_entry.data["reply"]["text"],
  408. }
  409. except KeyError:
  410. return _("Deleted a reply")
  411. @hooks.register("register_log_actions")
  412. def register_workflow_log_actions(actions):
  413. class WorkflowLogFormatter(LogFormatter):
  414. def format_comment(self, log_entry):
  415. return log_entry.data.get("comment", "")
  416. @actions.register_action("wagtail.workflow.start")
  417. class StartWorkflowActionFormatter(WorkflowLogFormatter):
  418. label = _("Workflow: start")
  419. def format_message(self, log_entry):
  420. try:
  421. return _("'%(workflow)s' started. Next step '%(task)s'") % {
  422. "workflow": log_entry.data["workflow"]["title"],
  423. "task": log_entry.data["workflow"]["next"]["title"],
  424. }
  425. except (KeyError, TypeError):
  426. return _("Workflow started")
  427. @actions.register_action("wagtail.workflow.approve")
  428. class ApproveWorkflowActionFormatter(WorkflowLogFormatter):
  429. label = _("Workflow: approve task")
  430. def format_message(self, log_entry):
  431. try:
  432. if log_entry.data["workflow"]["next"]:
  433. return _("Approved at '%(task)s'. Next step '%(next_task)s'") % {
  434. "task": log_entry.data["workflow"]["task"]["title"],
  435. "next_task": log_entry.data["workflow"]["next"]["title"],
  436. }
  437. else:
  438. return _("Approved at '%(task)s'. '%(workflow)s' complete") % {
  439. "task": log_entry.data["workflow"]["task"]["title"],
  440. "workflow": log_entry.data["workflow"]["title"],
  441. }
  442. except (KeyError, TypeError):
  443. return _("Workflow task approved")
  444. @actions.register_action("wagtail.workflow.reject")
  445. class RejectWorkflowActionFormatter(WorkflowLogFormatter):
  446. label = _("Workflow: reject task")
  447. def format_message(self, log_entry):
  448. try:
  449. return _("Rejected at '%(task)s'. Changes requested") % {
  450. "task": log_entry.data["workflow"]["task"]["title"],
  451. }
  452. except (KeyError, TypeError):
  453. return _("Workflow task rejected. Workflow complete")
  454. @actions.register_action("wagtail.workflow.resume")
  455. class ResumeWorkflowActionFormatter(WorkflowLogFormatter):
  456. label = _("Workflow: resume task")
  457. def format_message(self, log_entry):
  458. try:
  459. return _("Resubmitted '%(task)s'. Workflow resumed") % {
  460. "task": log_entry.data["workflow"]["task"]["title"],
  461. }
  462. except (KeyError, TypeError):
  463. return _("Workflow task resubmitted. Workflow resumed")
  464. @actions.register_action("wagtail.workflow.cancel")
  465. class CancelWorkflowActionFormatter(WorkflowLogFormatter):
  466. label = _("Workflow: cancel")
  467. def format_message(self, log_entry):
  468. try:
  469. return _("Cancelled '%(workflow)s' at '%(task)s'") % {
  470. "workflow": log_entry.data["workflow"]["title"],
  471. "task": log_entry.data["workflow"]["task"]["title"],
  472. }
  473. except (KeyError, TypeError):
  474. return _("Workflow cancelled")