wagtail/wagtail/wagtailadmin/views/pages.py
Martin Hill 9f5ce0755a Fixed RuntimeError: dictionary changed size during iteration, raised while purging stale preview data from session
Added TestPreview.test_preview_on_edit_expiry to exercise PreviewOnEdit.remove_old_preview_data
Added freezegun package in testing_extras
2017-05-10 11:28:45 +02:00

1128 lines
42 KiB
Python

from __future__ import absolute_import, unicode_literals
from time import time
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.db.models import Count
from django.http import Http404, HttpResponse, JsonResponse
from django.http.request import QueryDict
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.http import is_safe_url, urlquote
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_GET, require_POST
from django.views.decorators.vary import vary_on_headers
from django.views.generic import View
from wagtail.utils.pagination import paginate
from wagtail.wagtailadmin import messages, signals
from wagtail.wagtailadmin.forms import CopyForm, SearchForm
from wagtail.wagtailadmin.navigation import get_navigation_menu_items
from wagtail.wagtailadmin.utils import (
send_notification, user_has_any_page_permission, user_passes_test)
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision, UserPagePermissionsProxy
def get_valid_next_url_from_request(request):
next_url = request.POST.get('next') or request.GET.get('next')
if not next_url or not is_safe_url(url=next_url, host=request.get_host()):
return ''
return next_url
@user_passes_test(user_has_any_page_permission)
def explorer_nav(request):
return render(request, 'wagtailadmin/shared/explorer_nav.html', {
'nodes': get_navigation_menu_items(request.user),
})
@user_passes_test(user_has_any_page_permission)
def index(request, parent_page_id=None):
if parent_page_id:
parent_page = get_object_or_404(Page, id=parent_page_id).specific
else:
parent_page = Page.get_first_root_node().specific
pages = parent_page.get_children().prefetch_related('content_type', 'sites_rooted_here')
# Get page ordering
ordering = request.GET.get('ordering', '-latest_revision_created_at')
if ordering not in [
'title',
'-title',
'content_type',
'-content_type',
'live', '-live',
'latest_revision_created_at',
'-latest_revision_created_at',
'ord'
]:
ordering = '-latest_revision_created_at'
if ordering == 'ord':
# preserve the native ordering from get_children()
pass
elif ordering == 'latest_revision_created_at':
# order by oldest revision first.
# Special case NULL entries - these should go at the top of the list.
# Do this by annotating with Count('latest_revision_created_at'),
# which returns 0 for these
pages = pages.annotate(
null_position=Count('latest_revision_created_at')
).order_by('null_position', 'latest_revision_created_at')
elif ordering == '-latest_revision_created_at':
# order by oldest revision first.
# Special case NULL entries - these should go at the end of the list.
pages = pages.annotate(
null_position=Count('latest_revision_created_at')
).order_by('-null_position', '-latest_revision_created_at')
else:
pages = pages.order_by(ordering)
# Don't paginate if sorting by page order - all pages must be shown to
# allow drag-and-drop reordering
do_paginate = ordering != 'ord'
if do_paginate:
# Retrieve pages in their most specific form.
# Only do this for paginated listings, as this could potentially be a
# very expensive operation when performed on a large queryset.
pages = pages.specific()
# allow hooks to modify the queryset
for hook in hooks.get_hooks('construct_explorer_page_queryset'):
pages = hook(parent_page, pages, request)
# Pagination
if do_paginate:
paginator, pages = paginate(request, pages, per_page=50)
return render(request, 'wagtailadmin/pages/index.html', {
'parent_page': parent_page.specific,
'ordering': ordering,
'pagination_query_params': "ordering=%s" % ordering,
'pages': pages,
'do_paginate': do_paginate,
})
def add_subpage(request, parent_page_id):
parent_page = get_object_or_404(Page, id=parent_page_id).specific
if not parent_page.permissions_for_user(request.user).can_add_subpage():
raise PermissionDenied
page_types = [
(model.get_verbose_name(), model._meta.app_label, model._meta.model_name)
for model in type(parent_page).creatable_subpage_models()
if model.can_create_at(parent_page)
]
# sort by lower-cased version of verbose name
page_types.sort(key=lambda page_type: page_type[0].lower())
if len(page_types) == 1:
# Only one page type is available - redirect straight to the create form rather than
# making the user choose
verbose_name, app_label, model_name = page_types[0]
return redirect('wagtailadmin_pages:add', app_label, model_name, parent_page.id)
return render(request, 'wagtailadmin/pages/add_subpage.html', {
'parent_page': parent_page,
'page_types': page_types,
'next': get_valid_next_url_from_request(request),
})
def content_type_use(request, content_type_app_name, content_type_model_name):
try:
content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
except ContentType.DoesNotExist:
raise Http404
page_class = content_type.model_class()
# page_class must be a Page type and not some other random model
if not issubclass(page_class, Page):
raise Http404
pages = page_class.objects.all()
paginator, pages = paginate(request, pages, per_page=10)
return render(request, 'wagtailadmin/pages/content_type_use.html', {
'pages': pages,
'app_name': content_type_app_name,
'content_type': content_type,
'page_class': page_class,
})
def create(request, content_type_app_name, content_type_model_name, parent_page_id):
parent_page = get_object_or_404(Page, id=parent_page_id).specific
parent_page_perms = parent_page.permissions_for_user(request.user)
if not parent_page_perms.can_add_subpage():
raise PermissionDenied
try:
content_type = ContentType.objects.get_by_natural_key(content_type_app_name, content_type_model_name)
except ContentType.DoesNotExist:
raise Http404
# Get class
page_class = content_type.model_class()
# Make sure the class is a descendant of Page
if not issubclass(page_class, Page):
raise Http404
# page must be in the list of allowed subpage types for this parent ID
if page_class not in parent_page.creatable_subpage_models():
raise PermissionDenied
if not page_class.can_create_at(parent_page):
raise PermissionDenied
for fn in hooks.get_hooks('before_create_page'):
result = fn(request, parent_page, page_class)
if hasattr(result, 'status_code'):
return result
page = page_class(owner=request.user)
edit_handler_class = page_class.get_edit_handler()
form_class = edit_handler_class.get_form_class(page_class)
next_url = get_valid_next_url_from_request(request)
if request.method == 'POST':
form = form_class(request.POST, request.FILES, instance=page,
parent_page=parent_page)
if form.is_valid():
page = form.save(commit=False)
is_publishing = bool(request.POST.get('action-publish')) and parent_page_perms.can_publish_subpage()
is_submitting = bool(request.POST.get('action-submit'))
if not is_publishing:
page.live = False
# Save page
parent_page.add_child(instance=page)
# Save revision
revision = page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
)
# Publish
if is_publishing:
revision.publish()
# Notifications
if is_publishing:
if page.go_live_at and page.go_live_at > timezone.now():
messages.success(request, _("Page '{0}' created and scheduled for publishing.").format(page.get_admin_display_title()), buttons=[
messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit'))
])
else:
messages.success(request, _("Page '{0}' created and published.").format(page.get_admin_display_title()), buttons=[
messages.button(page.url, _('View live'), new_window=True),
messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit'))
])
elif is_submitting:
messages.success(
request,
_("Page '{0}' created and submitted for moderation.").format(page.get_admin_display_title()),
buttons=[
messages.button(
reverse('wagtailadmin_pages:view_draft', args=(page.id,)),
_('View draft'),
new_window=True
),
messages.button(
reverse('wagtailadmin_pages:edit', args=(page.id,)),
_('Edit')
)
]
)
if not send_notification(page.get_latest_revision().id, 'submitted', request.user.pk):
messages.error(request, _("Failed to send notifications to moderators"))
else:
messages.success(request, _("Page '{0}' created.").format(page.get_admin_display_title()))
for fn in hooks.get_hooks('after_create_page'):
result = fn(request, page)
if hasattr(result, 'status_code'):
return result
if is_publishing or is_submitting:
# we're done here
if next_url:
# redirect back to 'next' url if present
return redirect(next_url)
# redirect back to the explorer
return redirect('wagtailadmin_explore', page.get_parent().id)
else:
# Just saving - remain on edit page for further edits
target_url = reverse('wagtailadmin_pages:edit', args=[page.id])
if next_url:
# Ensure the 'next' url is passed through again if present
target_url += '?next=%s' % urlquote(next_url)
return redirect(target_url)
else:
messages.validation_error(
request, _("The page could not be created due to validation errors"), form
)
edit_handler = edit_handler_class(instance=page, form=form)
has_unsaved_changes = True
else:
signals.init_new_page.send(sender=create, page=page, parent=parent_page)
form = form_class(instance=page, parent_page=parent_page)
edit_handler = edit_handler_class(instance=page, form=form)
has_unsaved_changes = False
return render(request, 'wagtailadmin/pages/create.html', {
'content_type': content_type,
'page_class': page_class,
'parent_page': parent_page,
'edit_handler': edit_handler,
'preview_modes': page.preview_modes,
'form': form,
'next': next_url,
'has_unsaved_changes': has_unsaved_changes,
})
def edit(request, page_id):
latest_revision = get_object_or_404(Page, id=page_id).get_latest_revision()
page = get_object_or_404(Page, id=page_id).get_latest_revision_as_page()
parent = page.get_parent()
content_type = ContentType.objects.get_for_model(page)
page_class = content_type.model_class()
page_perms = page.permissions_for_user(request.user)
if not page_perms.can_edit():
raise PermissionDenied
for fn in hooks.get_hooks('before_edit_page'):
result = fn(request, page)
if hasattr(result, 'status_code'):
return result
edit_handler_class = page_class.get_edit_handler()
form_class = edit_handler_class.get_form_class(page_class)
next_url = get_valid_next_url_from_request(request)
errors_debug = None
if request.method == 'POST':
form = form_class(request.POST, request.FILES, instance=page,
parent_page=parent)
if form.is_valid() and not page.locked:
page = form.save(commit=False)
is_publishing = bool(request.POST.get('action-publish')) and page_perms.can_publish()
is_submitting = bool(request.POST.get('action-submit'))
is_reverting = bool(request.POST.get('revision'))
# If a revision ID was passed in the form, get that revision so its
# date can be referenced in notification messages
if is_reverting:
previous_revision = get_object_or_404(page.revisions, id=request.POST.get('revision'))
# Save revision
revision = page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
)
# Publish
if is_publishing:
revision.publish()
# Need to reload the page because the URL may have changed, and we
# need the up-to-date URL for the "View Live" button.
page = page.specific_class.objects.get(pk=page.pk)
# Notifications
if is_publishing:
if page.go_live_at and page.go_live_at > timezone.now():
# Page has been scheduled for publishing in the future
if is_reverting:
message = _(
"Revision from {0} of page '{1}' has been scheduled for publishing."
).format(
previous_revision.created_at.strftime("%d %b %Y %H:%M"),
page.get_admin_display_title()
)
else:
message = _(
"Page '{0}' has been scheduled for publishing."
).format(
page.get_admin_display_title()
)
messages.success(request, message, buttons=[
messages.button(
reverse('wagtailadmin_pages:edit', args=(page.id,)),
_('Edit')
)
])
else:
# Page is being published now
if is_reverting:
message = _(
"Revision from {0} of page '{1}' has been published."
).format(
previous_revision.created_at.strftime("%d %b %Y %H:%M"),
page.get_admin_display_title()
)
else:
message = _(
"Page '{0}' has been published."
).format(
page.get_admin_display_title()
)
messages.success(request, message, buttons=[
messages.button(
page.url,
_('View live'),
new_window=True
),
messages.button(
reverse('wagtailadmin_pages:edit', args=(page_id,)),
_('Edit')
)
])
elif is_submitting:
message = _(
"Page '{0}' has been submitted for moderation."
).format(
page.get_admin_display_title()
)
messages.success(request, message, buttons=[
messages.button(
reverse('wagtailadmin_pages:view_draft', args=(page_id,)),
_('View draft'),
new_window=True
),
messages.button(
reverse('wagtailadmin_pages:edit', args=(page_id,)),
_('Edit')
)
])
if not send_notification(page.get_latest_revision().id, 'submitted', request.user.pk):
messages.error(request, _("Failed to send notifications to moderators"))
else: # Saving
if is_reverting:
message = _(
"Page '{0}' has been replaced with revision from {1}."
).format(
page.get_admin_display_title(),
previous_revision.created_at.strftime("%d %b %Y %H:%M")
)
else:
message = _(
"Page '{0}' has been updated."
).format(
page.get_admin_display_title()
)
messages.success(request, message)
for fn in hooks.get_hooks('after_edit_page'):
result = fn(request, page)
if hasattr(result, 'status_code'):
return result
if is_publishing or is_submitting:
# we're done here - redirect back to the explorer
if next_url:
# redirect back to 'next' url if present
return redirect(next_url)
# redirect back to the explorer
return redirect('wagtailadmin_explore', page.get_parent().id)
else:
# Just saving - remain on edit page for further edits
target_url = reverse('wagtailadmin_pages:edit', args=[page.id])
if next_url:
# Ensure the 'next' url is passed through again if present
target_url += '?next=%s' % urlquote(next_url)
return redirect(target_url)
else:
if page.locked:
messages.error(request, _("The page could not be saved as it is locked"))
else:
messages.validation_error(
request, _("The page could not be saved due to validation errors"), form
)
edit_handler = edit_handler_class(instance=page, form=form)
errors_debug = (
repr(edit_handler.form.errors) +
repr([
(name, formset.errors)
for (name, formset) in edit_handler.form.formsets.items()
if formset.errors
])
)
has_unsaved_changes = True
else:
form = form_class(instance=page, parent_page=parent)
edit_handler = edit_handler_class(instance=page, form=form)
has_unsaved_changes = False
# Check for revisions still undergoing moderation and warn
if latest_revision and latest_revision.submitted_for_moderation:
buttons = []
if page.live:
buttons.append(messages.button(
reverse('wagtailadmin_pages:revisions_compare', args=(page.id, 'live', latest_revision.id)),
_('Compare with live version')
))
messages.warning(request, _("This page is currently awaiting moderation"), buttons=buttons)
return render(request, 'wagtailadmin/pages/edit.html', {
'page': page,
'content_type': content_type,
'edit_handler': edit_handler,
'errors_debug': errors_debug,
'preview_modes': page.preview_modes,
'form': form,
'next': next_url,
'has_unsaved_changes': has_unsaved_changes,
})
def delete(request, page_id):
page = get_object_or_404(Page, id=page_id)
if not page.permissions_for_user(request.user).can_delete():
raise PermissionDenied
for fn in hooks.get_hooks('before_delete_page'):
result = fn(request, page)
if hasattr(result, 'status_code'):
return result
next_url = get_valid_next_url_from_request(request)
if request.method == 'POST':
parent_id = page.get_parent().id
page.delete()
messages.success(request, _("Page '{0}' deleted.").format(page.get_admin_display_title()))
for fn in hooks.get_hooks('after_delete_page'):
result = fn(request, page)
if hasattr(result, 'status_code'):
return result
if next_url:
return redirect(next_url)
return redirect('wagtailadmin_explore', parent_id)
return render(request, 'wagtailadmin/pages/confirm_delete.html', {
'page': page,
'descendant_count': page.get_descendant_count(),
'next': next_url,
})
def view_draft(request, page_id):
page = get_object_or_404(Page, id=page_id).get_latest_revision_as_page()
perms = page.permissions_for_user(request.user)
if not (perms.can_publish() or perms.can_edit()):
raise PermissionDenied
return page.serve_preview(page.dummy_request(request), page.default_preview_mode)
class PreviewOnEdit(View):
http_method_names = ('post', 'get')
preview_expiration_timeout = 60 * 60 * 24 # seconds
session_key_prefix = 'wagtail-preview-'
def remove_old_preview_data(self):
expiration = time() - self.preview_expiration_timeout
expired_keys = [
k for k, v in self.request.session.items()
if k.startswith(self.session_key_prefix) and v[1] < expiration]
# Removes the session key gracefully
for k in expired_keys:
self.request.session.pop(k)
@property
def session_key(self):
return self.session_key_prefix + ','.join(self.args)
def get_page(self):
return get_object_or_404(Page,
id=self.args[0]).get_latest_revision_as_page()
def get_form(self):
page = self.get_page()
form_class = page.get_edit_handler().get_form_class(page._meta.model)
parent_page = page.get_parent().specific
post_data_dict, timestamp = self.request.session[self.session_key]
# convert post_data_dict back into a QueryDict
post_data = QueryDict('', mutable=True)
for k, v in post_data_dict.items():
post_data.setlist(k, v)
return form_class(post_data, instance=page, parent_page=parent_page)
def post(self, request, *args, **kwargs):
# TODO: Handle request.FILES.
# Convert request.POST to a plain dict (rather than a QueryDict) so that it can be
# stored without data loss in session data
post_data_dict = dict(request.POST.lists())
request.session[self.session_key] = post_data_dict, time()
self.remove_old_preview_data()
form = self.get_form()
return JsonResponse({'is_valid': form.is_valid()})
def error_response(self, page):
return render(self.request, 'wagtailadmin/pages/preview_error.html',
{'page': page})
def get(self, request, *args, **kwargs):
# Receive the form submission that would typically be posted
# to the view. If submission is valid, return the rendered page;
# if not, re-render the edit form
form = self.get_form()
page = form.instance
if form.is_valid():
form.save(commit=False)
preview_mode = request.GET.get('mode', page.default_preview_mode)
return page.serve_preview(page.dummy_request(request),
preview_mode)
return self.error_response(page)
class PreviewOnCreate(PreviewOnEdit):
def get_page(self):
(content_type_app_name, content_type_model_name,
parent_page_id) = self.args
try:
content_type = ContentType.objects.get_by_natural_key(
content_type_app_name, content_type_model_name)
except ContentType.DoesNotExist:
raise Http404
page = content_type.model_class()()
parent_page = get_object_or_404(Page, id=parent_page_id).specific
# We need to populate treebeard's path / depth fields in order to
# pass validation. We can't make these 100% consistent with the rest
# of the tree without making actual database changes (such as
# incrementing the parent's numchild field), but by calling treebeard's
# internal _get_path method, we can set a 'realistic' value that will
# hopefully enable tree traversal operations
# to at least partially work.
page.depth = parent_page.depth + 1
# Puts the page at the maximum possible path
# for a child of `parent_page`.
page.path = Page._get_children_path_interval(parent_page.path)[1]
return page
def get_form(self):
form = super(PreviewOnCreate, self).get_form()
if form.is_valid():
# Ensures our unsaved page has a suitable url.
form.instance.set_url_path(form.parent_page)
form.instance.full_clean()
return form
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 move_choose_destination(request, page_to_move_id, viewed_page_id=None):
page_to_move = get_object_or_404(Page, id=page_to_move_id)
page_perms = page_to_move.permissions_for_user(request.user)
if not page_perms.can_move():
raise PermissionDenied
if viewed_page_id:
viewed_page = get_object_or_404(Page, id=viewed_page_id)
else:
viewed_page = Page.get_first_root_node()
viewed_page.can_choose = page_perms.can_move_to(viewed_page)
child_pages = []
for target in viewed_page.get_children():
# can't move the page into itself or its descendants
target.can_choose = page_perms.can_move_to(target)
target.can_descend = (
not(target == page_to_move or
target.is_child_of(page_to_move)) and
target.get_children_count()
)
child_pages.append(target)
# Pagination
paginator, child_pages = paginate(request, child_pages, per_page=50)
return render(request, 'wagtailadmin/pages/move_choose_destination.html', {
'page_to_move': page_to_move,
'viewed_page': viewed_page,
'child_pages': child_pages,
})
def move_confirm(request, page_to_move_id, destination_id):
page_to_move = get_object_or_404(Page, id=page_to_move_id).specific
destination = get_object_or_404(Page, id=destination_id)
if not page_to_move.permissions_for_user(request.user).can_move_to(destination):
raise PermissionDenied
if request.method == 'POST':
# any invalid moves *should* be caught by the permission check above,
# so don't bother to catch InvalidMoveToDescendant
page_to_move.move(destination, pos='last-child')
messages.success(request, _("Page '{0}' moved.").format(page_to_move.get_admin_display_title()), buttons=[
messages.button(reverse('wagtailadmin_pages:edit', args=(page_to_move.id,)), _('Edit'))
])
return redirect('wagtailadmin_explore', destination.id)
return render(request, 'wagtailadmin/pages/confirm_move.html', {
'page_to_move': page_to_move,
'destination': destination,
})
def set_page_position(request, page_to_move_id):
page_to_move = get_object_or_404(Page, id=page_to_move_id)
parent_page = page_to_move.get_parent()
if not parent_page.permissions_for_user(request.user).can_reorder_children():
raise PermissionDenied
if request.method == 'POST':
# Get position parameter
position = request.GET.get('position', None)
# Find page thats already in this position
position_page = None
if position is not None:
try:
position_page = parent_page.get_children()[int(position)]
except IndexError:
pass # No page in this position
# Move page
# any invalid moves *should* be caught by the permission check above,
# so don't bother to catch InvalidMoveToDescendant
if position_page:
# If the page has been moved to the right, insert it to the
# right. If left, then left.
old_position = list(parent_page.get_children()).index(page_to_move)
if int(position) < old_position:
page_to_move.move(position_page, pos='left')
elif int(position) > old_position:
page_to_move.move(position_page, pos='right')
else:
# Move page to end
page_to_move.move(parent_page, pos='last-child')
return HttpResponse('')
@user_passes_test(user_has_any_page_permission)
def copy(request, page_id):
page = Page.objects.get(id=page_id)
# Parent page defaults to parent of source page
parent_page = page.get_parent()
# Check if the user has permission to publish subpages on the parent
can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()
# Create the form
form = CopyForm(request.POST or None, page=page, can_publish=can_publish)
next_url = get_valid_next_url_from_request(request)
for fn in hooks.get_hooks('before_copy_page'):
result = fn(request, page)
if hasattr(result, 'status_code'):
return result
# Check if user is submitting
if request.method == 'POST':
# Prefill parent_page in case the form is invalid (as prepopulated value for the form field,
# because ModelChoiceField seems to not fall back to the user given value)
parent_page = Page.objects.get(id=request.POST['new_parent_page'])
if form.is_valid():
# Receive the parent page (this should never be empty)
if form.cleaned_data['new_parent_page']:
parent_page = form.cleaned_data['new_parent_page']
if not page.permissions_for_user(request.user).can_copy_to(parent_page,
form.cleaned_data.get('copy_subpages')):
raise PermissionDenied
# Re-check if the user has permission to publish subpages on the new parent
can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()
# Copy the page
new_page = page.copy(
recursive=form.cleaned_data.get('copy_subpages'),
to=parent_page,
update_attrs={
'title': form.cleaned_data['new_title'],
'slug': form.cleaned_data['new_slug'],
},
keep_live=(can_publish and form.cleaned_data.get('publish_copies')),
user=request.user,
)
# Give a success message back to the user
if form.cleaned_data.get('copy_subpages'):
messages.success(
request,
_("Page '{0}' and {1} subpages copied.").format(page.get_admin_display_title(), new_page.get_descendants().count())
)
else:
messages.success(request, _("Page '{0}' copied.").format(page.get_admin_display_title()))
for fn in hooks.get_hooks('after_copy_page'):
result = fn(request, page, new_page)
if hasattr(result, 'status_code'):
return result
# Redirect to explore of parent page
if next_url:
return redirect(next_url)
return redirect('wagtailadmin_explore', parent_page.id)
return render(request, 'wagtailadmin/pages/copy.html', {
'page': page,
'form': form,
'next': next_url,
})
@vary_on_headers('X-Requested-With')
@user_passes_test(user_has_any_page_permission)
def search(request):
pages = []
q = None
if 'q' in request.GET:
form = SearchForm(request.GET)
if form.is_valid():
q = form.cleaned_data['q']
pages = Page.objects.all().prefetch_related('content_type').search(q)
paginator, pages = paginate(request, pages)
else:
form = SearchForm()
if request.is_ajax():
return render(request, "wagtailadmin/pages/search_results.html", {
'pages': pages,
'query_string': q,
'pagination_query_params': ('q=%s' % q) if q else ''
})
else:
return render(request, "wagtailadmin/pages/search.html", {
'search_form': form,
'pages': pages,
'query_string': q,
'pagination_query_params': ('q=%s' % q) if q else ''
})
def approve_moderation(request, revision_id):
revision = get_object_or_404(PageRevision, id=revision_id)
if not revision.page.permissions_for_user(request.user).can_publish():
raise PermissionDenied
if not revision.submitted_for_moderation:
messages.error(request, _("The page '{0}' is not currently awaiting moderation.").format(revision.page.get_admin_display_title()))
return redirect('wagtailadmin_home')
if request.method == 'POST':
revision.approve_moderation()
messages.success(request, _("Page '{0}' published.").format(revision.page.get_admin_display_title()), buttons=[
messages.button(revision.page.url, _('View live'), new_window=True),
messages.button(reverse('wagtailadmin_pages:edit', args=(revision.page.id,)), _('Edit'))
])
if not send_notification(revision.id, 'approved', request.user.pk):
messages.error(request, _("Failed to send approval notifications"))
return redirect('wagtailadmin_home')
def reject_moderation(request, revision_id):
revision = get_object_or_404(PageRevision, id=revision_id)
if not revision.page.permissions_for_user(request.user).can_publish():
raise PermissionDenied
if not revision.submitted_for_moderation:
messages.error(request, _("The page '{0}' is not currently awaiting moderation.").format(revision.page.get_admin_display_title()))
return redirect('wagtailadmin_home')
if request.method == 'POST':
revision.reject_moderation()
messages.success(request, _("Page '{0}' rejected for publication.").format(revision.page.get_admin_display_title()), buttons=[
messages.button(reverse('wagtailadmin_pages:edit', args=(revision.page.id,)), _('Edit'))
])
if not send_notification(revision.id, 'rejected', request.user.pk):
messages.error(request, _("Failed to send rejection notifications"))
return redirect('wagtailadmin_home')
@require_GET
def preview_for_moderation(request, revision_id):
revision = get_object_or_404(PageRevision, id=revision_id)
if not revision.page.permissions_for_user(request.user).can_publish():
raise PermissionDenied
if not revision.submitted_for_moderation:
messages.error(request, _("The page '{0}' is not currently awaiting moderation.").format(revision.page.get_admin_display_title()))
return redirect('wagtailadmin_home')
page = revision.as_page_object()
request.revision_id = revision_id
# pass in the real user request rather than page.dummy_request(), so that request.user
# and request.revision_id will be picked up by the wagtail user bar
return page.serve_preview(request, page.default_preview_mode)
@require_POST
def lock(request, page_id):
# Get the page
page = get_object_or_404(Page, id=page_id).specific
# Check permissions
if not page.permissions_for_user(request.user).can_lock():
raise PermissionDenied
# Lock the page
if not page.locked:
page.locked = True
page.save()
messages.success(request, _("Page '{0}' is now locked.").format(page.get_admin_display_title()))
# Redirect
redirect_to = request.POST.get('next', None)
if redirect_to and is_safe_url(url=redirect_to, host=request.get_host()):
return redirect(redirect_to)
else:
return redirect('wagtailadmin_explore', page.get_parent().id)
@require_POST
def unlock(request, page_id):
# Get the page
page = get_object_or_404(Page, id=page_id).specific
# Check permissions
if not page.permissions_for_user(request.user).can_lock():
raise PermissionDenied
# Unlock the page
if page.locked:
page.locked = False
page.save()
messages.success(request, _("Page '{0}' is now unlocked.").format(page.get_admin_display_title()))
# Redirect
redirect_to = request.POST.get('next', None)
if redirect_to and is_safe_url(url=redirect_to, host=request.get_host()):
return redirect(redirect_to)
else:
return redirect('wagtailadmin_explore', page.get_parent().id)
@user_passes_test(user_has_any_page_permission)
def revisions_index(request, page_id):
page = get_object_or_404(Page, id=page_id).specific
# Get page ordering
ordering = request.GET.get('ordering', '-created_at')
if ordering not in ['created_at', '-created_at', ]:
ordering = '-created_at'
revisions = page.revisions.order_by(ordering)
paginator, revisions = paginate(request, revisions)
return render(request, 'wagtailadmin/pages/revisions/index.html', {
'page': page,
'ordering': ordering,
'pagination_query_params': "ordering=%s" % ordering,
'revisions': revisions,
})
def revisions_revert(request, page_id, revision_id):
page = get_object_or_404(Page, id=page_id).specific
page_perms = page.permissions_for_user(request.user)
if not page_perms.can_edit():
raise PermissionDenied
revision = get_object_or_404(page.revisions, id=revision_id)
revision_page = revision.as_page_object()
content_type = ContentType.objects.get_for_model(page)
page_class = content_type.model_class()
edit_handler_class = page_class.get_edit_handler()
form_class = edit_handler_class.get_form_class(page_class)
form = form_class(instance=revision_page)
edit_handler = edit_handler_class(instance=revision_page, form=form)
user_avatar = render_to_string('wagtailadmin/shared/user_avatar.html', {'user': revision.user})
messages.warning(request, mark_safe(
_("You are viewing a previous revision of this page from <b>%(created_at)s</b> by %(user)s") % {
'created_at': revision.created_at.strftime("%d %b %Y %H:%M"),
'user': user_avatar,
}
))
return render(request, 'wagtailadmin/pages/edit.html', {
'page': page,
'revision': revision,
'is_revision': True,
'content_type': content_type,
'edit_handler': edit_handler,
'errors_debug': None,
'preview_modes': page.preview_modes,
'form': form, # Used in unit tests
})
@user_passes_test(user_has_any_page_permission)
def revisions_view(request, page_id, revision_id):
page = get_object_or_404(Page, id=page_id).specific
revision = get_object_or_404(page.revisions, id=revision_id)
revision_page = revision.as_page_object()
return revision_page.serve_preview(page.dummy_request(request), page.default_preview_mode)
def revisions_compare(request, page_id, revision_id_a, revision_id_b):
page = get_object_or_404(Page, id=page_id).specific
# Get revision to compare from
if revision_id_a == 'live':
if not page.live:
raise Http404
revision_a = page
revision_a_heading = _("Live")
elif revision_id_a == 'earliest':
revision_a = page.revisions.order_by('created_at', 'id').first()
if revision_a:
revision_a = revision_a.as_page_object()
revision_a_heading = _("Earliest")
else:
raise Http404
else:
revision_a = get_object_or_404(page.revisions, id=revision_id_a).as_page_object()
revision_a_heading = str(get_object_or_404(page.revisions, id=revision_id_a).created_at)
# Get revision to compare to
if revision_id_b == 'live':
if not page.live:
raise Http404
revision_b = page
revision_b_heading = _("Live")
elif revision_id_b == 'latest':
revision_b = page.revisions.order_by('created_at', 'id').last()
if revision_b:
revision_b = revision_b.as_page_object()
revision_b_heading = _("Latest")
else:
raise Http404
else:
revision_b = get_object_or_404(page.revisions, id=revision_id_b).as_page_object()
revision_b_heading = str(get_object_or_404(page.revisions, id=revision_id_b).created_at)
comparison = page.get_edit_handler().get_comparison()
comparison = [comp(revision_a, revision_b) for comp in comparison]
comparison = [comp for comp in comparison if comp.has_changed()]
return render(request, 'wagtailadmin/pages/revisions/compare.html', {
'page': page,
'revision_a_heading': revision_a_heading,
'revision_a': revision_a,
'revision_b_heading': revision_b_heading,
'revision_b': revision_b,
'comparison': comparison,
})