def publish(request, page_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_publish(): raise PermissionDenied next_url = get_valid_next_url_from_request(request) if request.method == 'POST': include_descendants = request.POST.get("include_descendants", False) page.save_revision().publish() if include_descendants: not_live_descendant_pages = ( page.get_descendants().not_live().specific()) for not_live_descendant_page in not_live_descendant_pages: if user_perms.for_page(not_live_descendant_page).can_publish(): not_live_descendant_page.save_revision().publish() if next_url: return redirect(next_url) return redirect('wagtailadmin_explore', page.get_parent().id) return render(request, 'wagtailadmin/pages/confirm_publish.html', { 'page': page, 'next': next_url, 'not_live_descendant_count': page.get_descendants().not_live().count() })
def publish(request, page_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_publish(): raise PermissionDenied next_url = get_valid_next_url_from_request(request) if request.method == 'POST': include_descendants = request.POST.get("include_descendants", False) page.save_revision().publish() if include_descendants: not_live_descendant_pages = ( page.get_descendants().not_live().specific()) for not_live_descendant_page in not_live_descendant_pages: if user_perms.for_page(not_live_descendant_page).can_publish(): not_live_descendant_page.save_revision().publish() if next_url: return redirect(next_url) return redirect('wagtailadmin_explore', page.get_parent().id) return render( request, 'wagtailadmin/pages/confirm_publish.html', { 'page': page, 'next': next_url, 'not_live_descendant_count': page.get_descendants().not_live().count() })
def unpublish(request, page_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_unpublish(): raise PermissionDenied next_url = get_valid_next_url_from_request(request) include_descendants = request.POST.get('include_descendants', False) page.unpublish() if include_descendants: live_descendant_pages = page.get_descendants().live().specific() for live_descendant_page in live_descendant_pages: if user_perms.for_page(live_descendant_page).can_unpublish(): live_descendant_page.unpublish() messages.success(request, _('Page \'{0}\' unpublished.').format( page.get_admin_display_title()), buttons=[ messages.button( reverse('wagtailadmin_pages:edit', args=(page.id, )), _('Edit')) ]) page.release_id = request.POST.get('release', None) page.save_revision(request.user) if next_url: return redirect(next_url) return redirect('wagtailadmin_explore', page.get_parent().id)
def unpublish(request, page_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_unpublish(): raise PermissionDenied next_url = get_valid_next_url_from_request(request) if request.method == 'POST': include_descendants = request.POST.get("include_descendants", False) page.unpublish() if include_descendants: live_descendant_pages = page.get_descendants().live().specific() for live_descendant_page in live_descendant_pages: if user_perms.for_page(live_descendant_page).can_unpublish(): live_descendant_page.unpublish() messages.success(request, _("Page '{0}' unpublished.").format(page.get_admin_display_title()), buttons=[ messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit')) ]) if next_url: return redirect(next_url) return redirect('wagtailadmin_explore', page.get_parent().id) return render(request, 'wagtailadmin/pages/confirm_unpublish.html', { 'page': page, 'next': next_url, 'live_descendant_count': page.get_descendants().live().count(), })
def unpublish(request, page_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_unpublish(): raise PermissionDenied next_url = get_valid_next_url_from_request(request) if request.method == 'POST': include_descendants = request.POST.get("include_descendants", False) for fn in hooks.get_hooks('before_unpublish_page'): result = fn(request, page) if hasattr(result, 'status_code'): return result page.unpublish(user=request.user) if include_descendants: for live_descendant_page in page.get_descendants().live( ).defer_streamfields().specific(): if user_perms.for_page(live_descendant_page).can_unpublish(): live_descendant_page.unpublish() for fn in hooks.get_hooks('after_unpublish_page'): result = fn(request, page) if hasattr(result, 'status_code'): return result messages.success(request, _("Page '{0}' unpublished.").format( page.get_admin_display_title()), buttons=[ messages.button( reverse('wagtailadmin_pages:edit', args=(page.id, )), _('Edit')) ]) if next_url: return redirect(next_url) return redirect('wagtailadmin_explore', page.get_parent().id) return TemplateResponse( request, 'wagtailadmin/pages/confirm_unpublish.html', { 'page': page, 'next': next_url, 'live_descendant_count': page.get_descendants().live().count(), })
def revisions_unschedule(request, page_id, revision_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_unschedule(): raise PermissionDenied revision = get_object_or_404(page.revisions, id=revision_id) next_url = get_valid_next_url_from_request(request) subtitle = _('revision {0} of "{1}"').format(revision.id, page.get_admin_display_title()) if request.method == 'POST': revision.approved_go_live_at = None revision.save(update_fields=['approved_go_live_at']) messages.success(request, _('Revision {0} of "{1}" unscheduled.').format(revision.id, page.get_admin_display_title()), buttons=[ messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit')) ]) if next_url: return redirect(next_url) return redirect('wagtailadmin_pages:revisions_index', page.id) return render(request, 'wagtailadmin/pages/revisions/confirm_unschedule.html', { 'page': page, 'revision': revision, 'next': next_url, 'subtitle': subtitle })
def publish(request, page_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_publish(): raise PermissionDenied next_url = pages.get_valid_next_url_from_request(request) if request.method == 'POST': page.get_latest_revision().publish() messages.success(request, _("Page '{0}' published.").format(page.get_admin_display_title()), buttons=[ messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit')) ]) if next_url: return redirect(next_url) return redirect('wagtailadmin_explore', page.get_parent().id) return render(request, 'wagtailadmin/pages/confirm_publish.html', { 'page': page, 'next': next_url, })
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ "page": self.page, "page_for_status": self.page_for_status, "content_type": self.page_content_type, "edit_handler": self.edit_handler, "errors_debug": self.errors_debug, "action_menu": PageActionMenu(self.request, view="edit", page=self.page), "preview_modes": self.page.preview_modes, "form": self.form, "next": self.next_url, "has_unsaved_changes": self.has_unsaved_changes, "page_locked": self.page_perms.page_locked(), "workflow_state": self.workflow_state if self.workflow_state and self.workflow_state.is_active else None, "current_task_state": self.page.current_workflow_task_state, "publishing_will_cancel_workflow": self.workflow_tasks and getattr(settings, "WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH", True), "locale": None, "translations": [], }) if getattr(settings, "WAGTAIL_I18N_ENABLED", False): user_perms = UserPagePermissionsProxy(self.request.user) context.update({ "locale": self.page.locale, "translations": [{ "locale": translation.locale, "url": reverse("wagtailadmin_pages:edit", args=[translation.id]), } for translation in self.page.get_translations().only( "id", "locale", "depth").select_related("locale") if user_perms.for_page(translation).can_edit()], }) return context
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'page': self.page, 'page_for_status': self.page_for_status, 'content_type': self.page_content_type, 'edit_handler': self.edit_handler, 'errors_debug': self.errors_debug, 'action_menu': PageActionMenu(self.request, view='edit', page=self.page), 'preview_modes': self.page.preview_modes, 'form': self.form, 'next': self.next_url, 'has_unsaved_changes': self.has_unsaved_changes, 'page_locked': self.page_perms.page_locked(), 'workflow_state': self.workflow_state if self.workflow_state and self.workflow_state.is_active else None, 'current_task_state': self.page.current_workflow_task_state, 'publishing_will_cancel_workflow': self.workflow_tasks and getattr(settings, 'WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH', True), 'locale': None, 'translations': [], }) if getattr(settings, 'WAGTAIL_I18N_ENABLED', False): user_perms = UserPagePermissionsProxy(self.request.user) context.update({ 'locale': self.page.locale, 'translations': [{ 'locale': translation.locale, 'url': reverse('wagtailadmin_pages:edit', args=[translation.id]), } for translation in self.page.get_translations().only( 'id', 'locale').select_related('locale') if user_perms.for_page(translation).can_edit()], }) return context
def __init__(self, request, **kwargs): self.request = request self.context = kwargs self.context["request"] = request page = self.context.get("page") user_page_permissions = UserPagePermissionsProxy(self.request.user) self.context["user_page_permissions"] = user_page_permissions if page: self.context[ "user_page_permissions_tester" ] = user_page_permissions.for_page(page) self.menu_items = [] if page: task = page.current_workflow_task current_workflow_state = page.current_workflow_state is_final_task = ( current_workflow_state and current_workflow_state.is_at_final_task ) if task: actions = task.get_actions(page, request.user) workflow_menu_items = [] for name, label, launch_modal in actions: icon_name = "edit" if name == "approve": if is_final_task and not getattr( settings, "WAGTAIL_WORKFLOW_REQUIRE_REAPPROVAL_ON_EDIT", False, ): label = _("%(label)s and Publish") % {"label": label} icon_name = "success" item = WorkflowMenuItem( name, label, launch_modal, icon_name=icon_name ) if item.is_shown(self.context): workflow_menu_items.append(item) self.menu_items.extend(workflow_menu_items) for menu_item in _get_base_page_action_menu_items(): if menu_item.is_shown(self.context): self.menu_items.append(menu_item) self.menu_items.sort(key=lambda item: item.order) for hook in hooks.get_hooks("construct_page_action_menu"): hook(self.menu_items, self.request, self.context) try: self.default_item = self.menu_items.pop(0) except IndexError: self.default_item = None
def user_keys_reject( request, user_id ): r_user = RUBIONUser.objects.get( id = user_id ) user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(r_user).can_edit(): raise PermissionDenied r_user.needs_key = False r_user.save_revision_and_publish( user = request.user ) messages.success(request, _('The application for a key for {} was rejected.').format( r_user.full_name() )) return redirect('wagtailadmin_home')
def unpublish(request, page_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_unpublish(): raise PermissionDenied next_url = get_valid_next_url_from_request(request) if request.method == "POST": include_descendants = request.POST.get("include_descendants", False) for fn in hooks.get_hooks("before_unpublish_page"): result = fn(request, page) if hasattr(result, "status_code"): return result action = UnpublishPageAction( page, user=request.user, include_descendants=include_descendants ) action.execute(skip_permission_checks=True) for fn in hooks.get_hooks("after_unpublish_page"): result = fn(request, page) if hasattr(result, "status_code"): return result messages.success( request, _("Page '{0}' unpublished.").format(page.get_admin_display_title()), buttons=[ messages.button( reverse("wagtailadmin_pages:edit", args=(page.id,)), _("Edit") ) ], ) if next_url: return redirect(next_url) return redirect("wagtailadmin_explore", page.get_parent().id) return TemplateResponse( request, "wagtailadmin/pages/confirm_unpublish.html", { "page": page, "next": next_url, "live_descendant_count": page.get_descendants().live().count(), }, )
def workflow_history(request, page_id): page = get_object_or_404(Page, id=page_id) user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_edit(): raise PermissionDenied workflow_states = WorkflowState.objects.filter(page=page).order_by('-created_at') paginator = Paginator(workflow_states, per_page=20) workflow_states = paginator.get_page(request.GET.get('p')) return TemplateResponse(request, 'wagtailadmin/pages/workflow_history/index.html', { 'page': page, 'workflow_states': workflow_states, })
def user_keys_accept( request, user_id ): r_user = RUBIONUser.objects.get( id = user_id ) user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(r_user).can_edit(): raise PermissionDenied num = request.POST.get('keyId', None) if num: r_user.key_number = num r_user.save_revision_and_publish( user = request.user ) messages.success(request, _('{} now was assigned the key with number {}.').format( r_user.full_name(), r_user.key_number )) return JsonResponse({'redirect' : reverse('wagtailadmin_home')}) else: return JsonResponse({'wrong' : 'Did not receive a keyId'})
def safety_instruction_helper(request, user_id, instruction_id): from .models import RUBIONUser r_user = get_object_or_404(RUBIONUser, pk=user_id) user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(r_user).can_edit() or not request.is_ajax(): raise PermissionDenied si = get_object_or_404(SafetyInstructionsSnippet, pk=instruction_id) instructions = r_user.needs_safety_instructions.all() response = { 'del_link': reverse('rubionadmin:user_safety_instruction_del', args=[user_id, instruction_id]), 'add_link': reverse('rubionadmin:user_safety_instruction_add', args=[user_id, instruction_id]) } return [r_user, si, instructions, response]
def revisions_unschedule(request, page_id, revision_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_unschedule(): raise PermissionDenied revision = get_object_or_404(page.revisions, id=revision_id) next_url = get_valid_next_url_from_request(request) subtitle = _('revision {0} of "{1}"').format( revision.id, page.get_admin_display_title()) if request.method == "POST": revision.approved_go_live_at = None revision.save(user=request.user, update_fields=["approved_go_live_at"]) messages.success( request, _('Version {0} of "{1}" unscheduled.').format( revision.id, page.get_admin_display_title()), buttons=[ messages.button( reverse("wagtailadmin_pages:edit", args=(page.id, )), _("Edit")) ], ) if next_url: return redirect(next_url) return redirect("wagtailadmin_pages:history", page.id) return TemplateResponse( request, "wagtailadmin/pages/revisions/confirm_unschedule.html", { "page": page, "revision": revision, "next": next_url, "subtitle": subtitle }, )
def execute(self, skip_permission_checks=False): self.check(skip_permission_checks=skip_permission_checks) self._unpublish_page( self.page, set_expired=self.set_expired, commit=self.commit, user=self.user, log_action=self.log_action, ) if self.include_descendants: from wagtail.core.models import UserPagePermissionsProxy user_perms = UserPagePermissionsProxy(self.user) for live_descendant_page in ( self.page.get_descendants().live().defer_streamfields().specific() ): action = UnpublishPageAction(live_descendant_page) if user_perms.for_page(live_descendant_page).can_unpublish(): action.execute(skip_permission_checks=True)
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'content_type': self.page_content_type, 'page_class': self.page_class, 'parent_page': self.parent_page, 'edit_handler': self.edit_handler, 'action_menu': PageActionMenu(self.request, view='create', parent_page=self.parent_page), 'preview_modes': self.page.preview_modes, 'form': self.form, 'next': self.next_url, 'has_unsaved_changes': self.has_unsaved_changes, 'locale': None, 'translations': [], }) if getattr(settings, 'WAGTAIL_I18N_ENABLED', False): # Pages can be created in any language at the root level if self.parent_page.is_root(): translations = [{ 'locale': locale, 'url': reverse( 'wagtailadmin_pages:add', args=[ self.page_content_type.app_label, self.page_content_type.model, self.parent_page.id ]) + '?' + urlencode({'locale': locale.language_code}), } for locale in Locale.objects.all()] else: user_perms = UserPagePermissionsProxy(self.request.user) translations = [ { 'locale': translation.locale, 'url': reverse('wagtailadmin_pages:add', args=[ self.page_content_type.app_label, self.page_content_type.model, translation.id ]), } for translation in self.parent_page.get_translations( ).only('id', 'locale').select_related('locale') if user_perms.for_page(translation).can_add_subpage() and self.page_class in translation.specific_class.creatable_subpage_models() and self.page_class.can_create_at(translation) ] context.update({ 'locale': self.locale, 'translations': translations, }) return context
def filter_pages_by_permission(user, pages): user_permissions = UserPagePermissionsProxy(user) return [ page for page in pages if user_permissions.for_page(page).can_add_subpage() ]
def publish(request, page_id): page = get_object_or_404(Page, id=page_id).specific user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_publish(): raise PermissionDenied next_url = pages.get_valid_next_url_from_request(request) if request.method == 'POST': page.get_latest_revision().publish() # TODO: clean up copypasta when coa-publisher phases out previously AWS publish pipeline # Show success message if there is a publish_janis_branch set (for netlify builds) # Or default to show success message on Staging and Production (follow existing AWS implementation pattern) try: publish_janis_branch = getattr(JanisBranchSettings.objects.first(), 'publish_janis_branch') except: publish_janis_branch = None if settings.ISSTAGING or settings.ISPRODUCTION: messages.success(request, _("Page '{0}' published.").format( page.get_admin_display_title()), buttons=[ messages.button( reverse('wagtailadmin_pages:edit', args=(page.id, )), _('Edit')) ]) elif settings.ISLOCAL: messages.warning( request, _("Page '{0}' not published. You're running on a local environment." ).format(page.get_admin_display_title()), buttons=[ messages.button( reverse('wagtailadmin_pages:edit', args=(page.id, )), _('Edit')) ]) elif publish_janis_branch: messages.success(request, _("Page '{0}' published.").format( page.get_admin_display_title()), buttons=[ messages.button( reverse('wagtailadmin_pages:edit', args=(page.id, )), _('Edit')) ]) else: messages.warning( request, _("Page '{0}' not published. No `publish_janis_branch` was set." ).format(page.get_admin_display_title()), buttons=[ messages.button( reverse('wagtailadmin_pages:edit', args=(page.id, )), _('Edit')) ]) if next_url: return redirect(next_url) # return redirect('wagtailadmin_explore', page.get_parent().id) return redirect('/admin/pages/search/') return render(request, 'wagtailadmin/pages/confirm_publish.html', { 'page': page, 'next': next_url, })
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update( { "content_type": self.page_content_type, "page_class": self.page_class, "parent_page": self.parent_page, "edit_handler": self.edit_handler, "action_menu": PageActionMenu( self.request, view="create", parent_page=self.parent_page ), "preview_modes": self.page.preview_modes, "form": self.form, "next": self.next_url, "has_unsaved_changes": self.has_unsaved_changes, "locale": None, "translations": [], } ) if getattr(settings, "WAGTAIL_I18N_ENABLED", False): # Pages can be created in any language at the root level if self.parent_page.is_root(): translations = [ { "locale": locale, "url": reverse( "wagtailadmin_pages:add", args=[ self.page_content_type.app_label, self.page_content_type.model, self.parent_page.id, ], ) + "?" + urlencode({"locale": locale.language_code}), } for locale in Locale.objects.all() ] else: user_perms = UserPagePermissionsProxy(self.request.user) translations = [ { "locale": translation.locale, "url": reverse( "wagtailadmin_pages:add", args=[ self.page_content_type.app_label, self.page_content_type.model, translation.id, ], ), } for translation in self.parent_page.get_translations() .only("id", "locale") .select_related("locale") if user_perms.for_page(translation).can_add_subpage() and self.page_class in translation.specific_class.creatable_subpage_models() and self.page_class.can_create_at(translation) ] context.update( { "locale": self.locale, "translations": translations, } ) return context
def __init__(self, request, **kwargs): self.request = request self.context = kwargs self.context['request'] = request page = self.context.get('page') user_page_permissions = UserPagePermissionsProxy(self.request.user) self.context['user_page_permissions'] = user_page_permissions if page: self.context[ 'user_page_permissions_tester'] = user_page_permissions.for_page( page) self.menu_items = [] if page: task = page.current_workflow_task current_workflow_state = page.current_workflow_state is_final_task = current_workflow_state and current_workflow_state.is_at_final_task if task: actions = task.get_actions(page, request.user) workflow_menu_items = [] for name, label, launch_modal in actions: icon_name = 'edit' if name == "approve": if is_final_task and not getattr( settings, 'WAGTAIL_WORKFLOW_REQUIRE_REAPPROVAL_ON_EDIT', False): label = _("%(label)s and Publish") % { 'label': label } icon_name = 'success' item = WorkflowMenuItem(name, label, launch_modal, icon_name=icon_name) if requires_request_arg(item.is_shown): warn( "%s.is_shown should no longer take a 'request' argument. " "See https://docs.wagtail.org/en/stable/releases/2.15.html#template-components-2-15" % type(item).__name__, category=RemovedInWagtail217Warning) is_shown = item.is_shown(self.request, self.context) else: is_shown = item.is_shown(self.context) if is_shown: workflow_menu_items.append(item) self.menu_items.extend(workflow_menu_items) for menu_item in _get_base_page_action_menu_items(): if requires_request_arg(menu_item.is_shown): warn( "%s.is_shown should no longer take a 'request' argument. " "See https://docs.wagtail.org/en/stable/releases/2.15.html#template-components-2-15" % type(menu_item).__name__, category=RemovedInWagtail217Warning) is_shown = menu_item.is_shown(self.request, self.context) else: is_shown = menu_item.is_shown(self.context) if is_shown: self.menu_items.append(menu_item) self.menu_items.sort(key=lambda item: item.order) for hook in hooks.get_hooks('construct_page_action_menu'): hook(self.menu_items, self.request, self.context) try: self.default_item = self.menu_items.pop(0) except IndexError: self.default_item = None
def workflow_history_detail(request, page_id, workflow_state_id): page = get_object_or_404(Page, id=page_id) user_perms = UserPagePermissionsProxy(request.user) if not user_perms.for_page(page).can_edit(): raise PermissionDenied workflow_state = get_object_or_404(WorkflowState, page=page, id=workflow_state_id) # Get QuerySet of all revisions that have existed during this workflow state # It's possible that the page is edited while the workflow is running, so some # tasks may be repeated. All tasks that have been completed no matter what # revision needs to be displayed on this page. page_revisions = PageRevision.objects.filter( page=page, id__in=TaskState.objects.filter( workflow_state=workflow_state).values_list("page_revision_id", flat=True), ).order_by("-created_at") # Now get QuerySet of tasks completed for each revision task_states_by_revision_task = [( page_revision, { task_state.task: task_state for task_state in TaskState.objects.filter( workflow_state=workflow_state, page_revision=page_revision) }, ) for page_revision in page_revisions] # Make sure task states are always in a consistent order # In some cases, they can be completed in a different order to what they are defined tasks = workflow_state.workflow.tasks.all() task_states_by_revision = [ (page_revision, [task_states_by_task.get(task, None) for task in tasks]) for page_revision, task_states_by_task in task_states_by_revision_task ] # Generate timeline completed_task_states = (TaskState.objects.filter( workflow_state=workflow_state).exclude( finished_at__isnull=True).exclude( status=TaskState.STATUS_CANCELLED)) timeline = [{ "time": workflow_state.created_at, "action": "workflow_started", "workflow_state": workflow_state, }] if workflow_state.status not in ( WorkflowState.STATUS_IN_PROGRESS, WorkflowState.STATUS_NEEDS_CHANGES, ): last_task = completed_task_states.order_by("finished_at").last() if last_task: timeline.append({ "time": last_task.finished_at + timedelta(milliseconds=1), "action": "workflow_completed", "workflow_state": workflow_state, }) for page_revision in page_revisions: timeline.append({ "time": page_revision.created_at, "action": "page_edited", "revision": page_revision, }) for task_state in completed_task_states: timeline.append({ "time": task_state.finished_at, "action": "task_completed", "task_state": task_state, }) timeline.sort(key=lambda t: t["time"]) timeline.reverse() return TemplateResponse( request, "wagtailadmin/pages/workflow_history/detail.html", { "page": page, "workflow_state": workflow_state, "tasks": tasks, "task_states_by_revision": task_states_by_revision, "timeline": timeline, }, )