divio/django-cms

View on GitHub
cms/admin/pageadmin.py

Summary

Maintainability
F
1 wk
Test Coverage
import json
import re
import sys
import uuid
from collections import namedtuple

import django
from django.conf import settings
from django.contrib import admin, messages
from django.contrib.admin.helpers import AdminForm
from django.contrib.admin.models import CHANGE, LogEntry
from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.utils import get_deleted_objects
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
from django.db import router, transaction
from django.db.models import Prefetch, Q
from django.http import (
    Http404,
    HttpResponse,
    HttpResponseBadRequest,
    HttpResponseForbidden,
    HttpResponseRedirect,
    QueryDict,
)
from django.shortcuts import get_object_or_404, render
from django.template.defaultfilters import escape
from django.template.loader import get_template
from django.template.response import SimpleTemplateResponse, TemplateResponse
from django.urls import NoReverseMatch, re_path
from django.utils.decorators import method_decorator
from django.utils.encoding import force_str
from django.utils.translation import get_language, gettext
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST

from cms import operations
from cms.admin.forms import (
    AddPageForm,
    AddPageTypeForm,
    AdvancedSettingsForm,
    ChangeListForm,
    ChangePageForm,
    CopyPageForm,
    CopyPermissionForm,
    DuplicatePageForm,
    MovePageForm,
    PagePermissionForm,
    PublicationDatesForm,
)
from cms.admin.permissionadmin import PERMISSION_ADMIN_INLINES
from cms.admin.placeholderadmin import PlaceholderAdminMixin
from cms.cache.permissions import clear_permission_cache
from cms.constants import PUBLISHER_STATE_PENDING
from cms.models import (
    CMSPlugin,
    EmptyTitle,
    GlobalPagePermission,
    Page,
    PagePermission,
    PageType,
    StaticPlaceholder,
    Title,
)
from cms.plugin_pool import plugin_pool
from cms.signals import post_obj_operation, pre_obj_operation
from cms.signals.apphook import set_restart_trigger
from cms.toolbar_pool import toolbar_pool
from cms.utils import copy_plugins, get_current_site, get_language_from_request, page_permissions, permissions
from cms.utils.admin import jsonify_request
from cms.utils.compat.response import get_response_headers
from cms.utils.conf import get_cms_setting
from cms.utils.i18n import get_language_list, get_language_object, get_language_tuple, get_site_language_from_request
from cms.utils.urlutils import admin_reverse

require_POST = method_decorator(require_POST)


PUBLISH_COMMENT = "Publish"


class TreeNodeAdmin(admin.ModelAdmin):
    list_filter = ['site']
    list_display = ['id', 'path', 'page', 'numchild', 'depth', 'parent_display', 'site']
    readonly_fields = ['parent', 'page']
    search_fields = (
        '=page__id',
        'page__title_set__slug',
        'page__title_set__title',
        'page__reverse_id',
    )

    def parent_display(self, obj):
        if obj.parent_id:
            return str(obj.parent)
        return ''
    parent_display.short_description = 'parent'


class BasePageAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
    form = AddPageForm
    ordering = ('node__path',)
    search_fields = ('=id', 'title_set__slug', 'title_set__title', 'reverse_id')
    add_general_fields = ['title', 'slug', 'language', 'template']
    change_list_template = "admin/cms/page/tree/base.html"
    actions_menu_template = 'admin/cms/page/tree/actions_dropdown.html'
    page_tree_row_template = 'admin/cms/page/tree/menu.html'
    title_frontend_editable_fields = ['title', 'menu_title', 'page_title']

    add_form = AddPageForm
    change_form = ChangePageForm
    copy_form = CopyPageForm
    advanced_form = AdvancedSettingsForm
    move_form = MovePageForm
    changelist_form = ChangeListForm
    duplicate_form = DuplicatePageForm

    inlines = PERMISSION_ADMIN_INLINES

    def get_admin_url(self, action, *args):
        url_name = "{}_{}_{}".format(
            self.opts.app_label,
            self.opts.model_name,
            action,
        )
        return admin_reverse(url_name, args=args)

    def get_queryset(self, request):
        site = self.get_site(request)
        queryset = super().get_queryset(request)
        queryset = queryset.filter(node__site=site, publisher_is_draft=True)
        return queryset.select_related('node')

    def get_object_with_translation(self, language, *args, **kwargs):
        page = self.get_object(*args, **kwargs)

        if page is None:
            return (None, None)

        try:
            translation = page.title_set.get(language=language)
        except Title.DoesNotExist:
            translation = None
        return (page, translation)

    def get_page_from_id(self, page_id):
        page_id = self.model._meta.pk.to_python(page_id)

        try:
            page = self.model.objects.get(
                pk=page_id,
                publisher_is_draft=True,
            )
        except self.model.DoesNotExist:
            page = None
        return page

    def get_site(self, request):
        site_id = request.session.get('cms_admin_site')

        if not site_id:
            return get_current_site()

        try:
            site = Site.objects._get_site_by_id(site_id)
        except Site.DoesNotExist:
            site = get_current_site()
        return site

    def get_urls(self):
        """Get the admin urls
        """
        info = f"{self.model._meta.app_label}_{self.model._meta.model_name}"

        def pat(regex, fn):
            return re_path(regex, self.admin_site.admin_view(fn), name=f'{info}_{fn.__name__}')

        url_patterns = [
            pat(r'^([0-9]+)/advanced-settings/$', self.advanced),
            pat(r'^([0-9]+)/duplicate/$', self.duplicate),
            pat(r'^([0-9]+)/actions-menu/$', self.actions_menu),
            pat(r'^([0-9]+)/dates/$', self.dates),
            pat(r'^([0-9]+)/permission-settings/$', self.permissions),
            pat(r'^([0-9]+)/delete-translation/$', self.delete_translation),
            pat(r'^([0-9]+)/move-page/$', self.move_page),
            pat(r'^([0-9]+)/copy-page/$', self.copy_page),
            pat(r'^([0-9]+)/copy-language/$', self.copy_language),
            pat(r'^([0-9]+)/dialog/copy/$', self.get_copy_dialog),  # copy dialog
            pat(r'^([0-9]+)/change-navigation/$', self.change_innavigation),
            pat(r'^([0-9]+)/permissions/$', self.get_permissions),
            pat(r'^([0-9]+)/change-template/$', self.change_template),
            pat(r'^([0-9]+)/([a-z\-]+)/edit-field/$', self.edit_title_fields),
            pat(r'^([0-9]+)/([a-z\-]+)/publish/$', self.publish_page),
            pat(r'^([0-9]+)/([a-z\-]+)/unpublish/$', self.unpublish),
            pat(r'^([0-9]+)/([a-z\-]+)/preview/$', self.preview_page),
            pat(r'^([0-9]+)/([a-z\-]+)/revert-to-live/$', self.revert_to_live),
            pat(r'^get-tree/$', self.get_tree),
        ]
        return url_patterns + super().get_urls()

    def _send_pre_page_operation(self, request, operation, **kwargs):
        token = str(uuid.uuid4())
        pre_obj_operation.send(
            sender=self.__class__,
            operation=operation,
            request=request,
            token=token,
            **kwargs
        )
        return token

    def _send_post_page_operation(self, request, operation, token, **kwargs):
        post_obj_operation.send(
            sender=self.__class__,
            operation=operation,
            request=request,
            token=token,
            **kwargs
        )

    def save_related(self, request, form, formsets, change):
        if hasattr(form, 'save_m2m'):
            form.save_m2m()

        for formset in formsets:
            self.save_formset(request, form, formset, change=change)

    def get_fieldsets(self, request, obj=None):
        form = self.get_form(request, obj, fields=None)

        try:
            fieldsets = form.fieldsets
        except AttributeError:
            fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj))
            fieldsets = [(None, {'fields': fields})]
        return fieldsets

    def get_inline_instances(self, request, obj=None):
        if obj and 'permission' in request.path_info:
            return super().get_inline_instances(request, obj)
        return []

    def get_form_class(self, request, obj=None, **kwargs):
        if 'advanced' in request.path_info:
            return self.advanced_form
        elif 'permission' in request.path_info:
            return PagePermissionForm
        elif 'dates' in request.path_info:
            return PublicationDatesForm
        elif 'change' in request.path_info or obj and obj.pk:
            return self.change_form
        elif 'duplicate' in request.path_info:
            return self.duplicate_form
        return self.add_form

    def get_form(self, request, obj=None, **kwargs):
        """
        Get PageForm for the Page model and modify its fields depending on
        the request.
        """
        form = super().get_form(
            request,
            obj,
            form=self.get_form_class(request, obj),
            **kwargs
        )
        form._user = request.user
        form._site = self.get_site(request)
        form._language = get_site_language_from_request(request, site_id=form._site.pk)
        return form

    def duplicate(self, request, object_id):
        """
        Leverages the add view logic to duplicate the page.
        """
        page = self.get_object(request, object_id=object_id)

        if page is None:
            raise self._get_404_exception(object_id)

        if request.method == 'GET':
            # source is a field in the form
            # because its value is in the url,
            # we have to set the initial value manually
            request.GET = request.GET.copy()
            request.GET['source'] = page.pk
        return self.add_view(request)

    def advanced(self, request, object_id):
        page = self.get_object(request, object_id=object_id)

        if not self.has_change_advanced_settings_permission(request, obj=page):
            raise PermissionDenied("No permission for editing advanced settings")

        if page is None:
            raise self._get_404_exception(object_id)

        # always returns a valid language
        site = self.get_site(request)
        language = get_site_language_from_request(request, site_id=site.pk)
        language_obj = get_language_object(language, site_id=site.pk)

        if not page.has_translation(language):
            # Can't edit advanced settings for a page translation (title)
            # that does not exist.
            message = _("Please create the %(language)s page "
                        "translation before editing it's advanced settings.")
            message = message % {'language': language_obj['name']}
            self.message_user(request, message, level=messages.ERROR)
            path = self.get_admin_url('change', object_id)
            return HttpResponseRedirect(f"{path}?language={language}")
        return self.change_view(request, object_id, extra_context={'advanced_settings': True, 'title': _("Advanced Settings")})

    def actions_menu(self, request, object_id, extra_context=None):
        page = self.get_object(request, object_id=object_id)

        if page is None:
            raise self._get_404_exception(object_id)

        site = self.get_site(request)
        paste_enabled = request.GET.get('has_copy') or request.GET.get('has_cut')
        context = {
            'page': page,
            'node': page.node,
            'opts': self.opts,
            'site': site,
            'page_is_restricted': page.has_view_restrictions(site),
            'paste_enabled': paste_enabled,
            'has_add_permission': page_permissions.user_can_add_subpage(request.user, target=page),
            'has_copy_page_permission': page_permissions.user_can_view_page_draft(request.user, page, site=site),
            'has_change_permission': self.has_change_permission(request, obj=page),
            'has_change_advanced_settings_permission': self.has_change_advanced_settings_permission(request, obj=page),
            'has_change_permissions_permission': self.has_change_permissions_permission(request, obj=page),
            'has_move_page_permission':  self.has_move_page_permission(request, obj=page),
            'has_delete_permission':  self.has_delete_permission(request, obj=page),
            'CMS_PERMISSION': get_cms_setting('PERMISSION'),
        }

        if extra_context:
            context.update(extra_context)
        return render(request, self.actions_menu_template, context)

    def dates(self, request, object_id):
        return self.change_view(request, object_id, extra_context={'publishing_dates': True, 'title': _("Publishing dates")})

    def permissions(self, request, object_id):
        page = self.get_object(request, object_id=object_id)

        if not self.has_change_permissions_permission(request, obj=page):
            raise PermissionDenied("No permission for editing advanced settings")

        if page is None:
            raise self._get_404_exception(object_id)
        return self.change_view(request, object_id, extra_context={'show_permissions': True, 'title': _("Change Permissions")})

    def add_view(self, request, form_url='', extra_context=None):
        if extra_context is None:
            extra_context = {}
        site = self.get_site(request)
        language = get_site_language_from_request(request, site_id=site.pk)
        extra_context.update({
            'language': language,
        })
        return super().add_view(request, form_url, extra_context=extra_context)

    def change_view(self, request, object_id, form_url='', extra_context=None):
        """
        The 'change' admin view for the Page model.
        """
        if extra_context is None:
            extra_context = {'basic_info': True}

        obj = self.get_object(request, object_id=object_id)

        if not self.has_change_permission(request, obj):
            raise PermissionDenied

        if obj is None:
            raise self._get_404_exception(object_id)

        context = {
            'page': obj,
            'CMS_PERMISSION': get_cms_setting('PERMISSION'),
            'can_change': self.has_change_permission(request, obj=obj),
            'can_change_permissions': self.has_change_permissions_permission(request, obj=obj),
        }
        context.update(extra_context or {})
        extra_context = self.update_language_tab_context(request, obj, context)

        if 'advanced_settings' in extra_context or 'basic_info' in extra_context:
            _has_advanced_settings_perm = self.has_change_advanced_settings_permission(request, obj=obj)
            extra_context['can_change_advanced_settings'] = _has_advanced_settings_perm

        site = self.get_site(request)
        tab_language = get_site_language_from_request(request, site_id=site.pk)

        response = super().change_view(
            request, object_id, form_url=form_url, extra_context=extra_context)
        response_headers = get_response_headers(response)
        if tab_language and response.status_code == 302 and response_headers['location'][1] == request.path_info:
            location = response_headers['location']
            response_headers['location'] = (location[0], f"{location[1]}?language={tab_language}")

        return response


    @transaction.atomic
    def delete_view(self, request, object_id, extra_context=None):
        # This is an unfortunate copy/paste from django's delete view.
        # The reason is to add the descendant pages to the deleted objects list.
        opts = self.model._meta
        app_label = opts.app_label

        obj = self.get_object(request, object_id=object_id)

        if not self.has_delete_permission(request, obj):
            raise PermissionDenied

        if obj is None:
            raise self._get_404_exception(object_id)

        router.db_for_write(self.model)

        # Populate deleted_objects, a data structure of all related objects that
        # will also be deleted.
        objs = [obj] + list(obj.get_descendant_pages())

        get_deleted_objects_additional_kwargs = {'request': request}
        (deleted_objects, model_count, perms_needed, protected) = get_deleted_objects(
            objs, admin_site=self.admin_site,
            **get_deleted_objects_additional_kwargs
        )

        if request.POST and not protected:  # The user has confirmed the deletion.
            if perms_needed:
                raise PermissionDenied
            obj_display = force_str(obj)
            obj_id = obj.serializable_value(opts.pk.attname)
            self.log_deletion(request, obj, obj_display)
            self.delete_model(request, obj)
            return self.response_delete(request, obj_display, obj_id)

        object_name = force_str(opts.verbose_name)

        if perms_needed or protected:
            title = _("Cannot delete %(name)s") % {"name": object_name}
        else:
            title = _("Are you sure?")

        context = dict(
            self.admin_site.each_context(request),
            title=title,
            object_name=object_name,
            object=obj,
            deleted_objects=deleted_objects,
            model_count=dict(model_count).items(),
            perms_lacking=perms_needed,
            protected=protected,
            opts=opts,
            app_label=app_label,
            preserved_filters=self.get_preserved_filters(request),
            is_popup=(
                IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET
            ),
            to_field=None,
        )
        context.update(extra_context or {})
        return self.render_delete_form(request, context)

    def delete_model(self, request, obj):
        operation_token = self._send_pre_page_operation(
            request,
            operation=operations.DELETE_PAGE,
            obj=obj,
        )

        cms_pages = [obj]

        if obj.publisher_public:
            cms_pages.append(obj.publisher_public)

        if obj.node.is_branch:
            nodes = obj.node.get_descendants()
            cms_pages.extend(self.model.objects.filter(node__in=nodes))

        for page in cms_pages:
            page._clear_placeholders()
            page.get_placeholders().delete()

        super().delete_model(request, obj)

        self._send_post_page_operation(
            request,
            operation=operations.DELETE_PAGE,
            token=operation_token,
            obj=obj,
        )

        clear_permission_cache()

        if obj.application_urls:
            set_restart_trigger()

    def get_copy_dialog(self, request, page_id):
        if not get_cms_setting('PERMISSION'):
            return HttpResponse('')

        page = self.get_page_from_id(page_id)

        if page is None:
            raise self._get_404_exception(page_id)

        if request.method == 'GET':
            data = request.GET
        else:
            data = request.POST

        target_id = data.get('target')

        try:
            source_site_id = data['source_site']
            source_site = Site.objects.get(pk=source_site_id)
        except (KeyError, ObjectDoesNotExist):
            return HttpResponseBadRequest('source_site is required')

        site = self.get_site(request)
        user = request.user
        can_view_page = page_permissions.user_can_view_page(user, page, source_site)

        if not can_view_page:
            raise PermissionDenied

        if target_id:
            try:
                target = Page.objects.get(pk=target_id)
            except Page.DoesNotExist:
                raise Http404

            if not page_permissions.user_can_add_subpage(user, target, site):
                raise PermissionDenied
        elif not page_permissions.user_can_add_page(user, site):
            raise PermissionDenied

        context = {
            'dialog_id': 'dialog-copy',
            'form': CopyPermissionForm(),  # class needs to be instantiated
            'opts': self.opts,
        }
        return render(request, "admin/cms/page/tree/copy_premissions.html", context)

    def get_filled_languages(self, request, obj):
        filled_languages = []

        if obj:
            filled_languages = [t[0] for t in obj.title_set.filter(title__isnull=False).values_list('language')]
        allowed_languages = [lang[0] for lang in self._get_site_languages(request, obj)]
        return [lang for lang in filled_languages if lang in allowed_languages]

    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
        context['filled_languages'] = self.get_filled_languages(request, obj)
        return super().render_change_form(request, context, add, change, form_url, obj)

    def _get_site_languages(self, request, obj=None):
        if obj:
            site_id = obj.node.site_id
        else:
            site_id = self.get_site(request).pk
        return get_language_tuple(site_id)

    def update_language_tab_context(self, request, obj=None, context=None):
        if context is None:
            context = {}

        site = self.get_site(request)
        language = get_site_language_from_request(request, site_id=site.pk)
        languages = self._get_site_languages(request, obj)
        context.update({
            'language': language,
            'language_tabs': languages,
            # Dates are not language dependent, thus we hide the language
            # selection bar: the language is forced through the form class
            'show_language_tabs': len(list(languages)) > 1 and not context.get('publishing_dates', False),
        })
        return context

    def get_preserved_filters(self, request):
        """
        This override is in place to preserve the "language" get parameter in
        the "Save" page redirect
        """
        preserved_filters_encoded = super().get_preserved_filters(request)
        preserved_filters = QueryDict(preserved_filters_encoded).copy()
        lang = request.GET.get('language')

        if lang:
            preserved_filters.update({
                'language': lang
            })

        return preserved_filters.urlencode()

    def _get_404_exception(self, object_id):
        exception = Http404(_('%(name)s object with primary key %(key)r does not exist.') % {
            'name': force_str(self.opts.verbose_name),
            'key': escape(object_id),
        })
        return exception

    def _has_add_permission_from_request(self, request):
        site = self.get_site(request)
        parent_node_id = request.GET.get('parent_node', None)

        if parent_node_id:
            try:
                parent_item = self.get_queryset(request).get(node=parent_node_id)
            except self.model.DoesNotExist:
                return False
        else:
            parent_item = None

        if parent_item:
            has_perm = page_permissions.user_can_add_subpage(
                request.user,
                target=parent_item,
                site=site,
            )
        else:
            has_perm = page_permissions.user_can_add_page(request.user, site=site)
        return has_perm

    def has_add_permission(self, request):
        """
        Return true if the current user has permission to add a new page.
        """
        return self._has_add_permission_from_request(request)

    def has_change_permission(self, request, obj=None):
        """
        Return true if the current user has permission on the page.
        Return the string 'All' if the user has all rights.
        """
        site = self.get_site(request)

        if obj:
            return page_permissions.user_can_change_page(request.user, page=obj, site=site)
        can_change_page = page_permissions.user_can_change_at_least_one_page(
            user=request.user,
            site=self.get_site(request),
            use_cache=False,
        )
        return can_change_page

    def has_view_permission(self, request, obj=None):
        return self.has_change_permission(request, obj)

    def has_change_advanced_settings_permission(self, request, obj=None):
        if not obj:
            return False
        site = self.get_site(request)
        return page_permissions.user_can_change_page_advanced_settings(request.user, page=obj, site=site)

    def has_change_permissions_permission(self, request, obj=None):
        if not obj:
            return False
        site = self.get_site(request)
        return page_permissions.user_can_change_page_permissions(request.user, page=obj, site=site)

    def has_delete_permission(self, request, obj=None):
        """
        Returns True if the current user has permission to delete the page.
        """
        if not obj:
            return False
        site = self.get_site(request)
        return page_permissions.user_can_delete_page(request.user, page=obj, site=site)

    def has_delete_translation_permission(self, request, language, obj=None):
        if not obj:
            return False

        site = self.get_site(request)
        has_perm = page_permissions.user_can_delete_page_translation(
            user=request.user,
            page=obj,
            language=language,
            site=site,
        )
        return has_perm

    def has_move_page_permission(self, request, obj=None):
        if not obj:
            return False
        site = self.get_site(request)
        return page_permissions.user_can_move_page(user=request.user, page=obj, site=site)

    def has_publish_permission(self, request, obj=None):
        if not obj:
            return False
        site = self.get_site(request)
        return page_permissions.user_can_publish_page(request.user, page=obj, site=site)

    def has_revert_to_live_permission(self, request, language, obj=None):
        if not obj:
            return False

        site = self.get_site(request)
        has_perm = page_permissions.user_can_revert_page_to_live(
            request.user,
            page=obj,
            language=language,
            site=site,
        )
        return has_perm

    def get_placeholder_template(self, request, placeholder):
        page = placeholder.page
        return page.get_template() if page else None

    def lookup_allowed(self, key, *args, **kwargs):
        if key == 'site__exact':
            return True
        return super().lookup_allowed(key, *args, **kwargs)

    def get_sites_for_user(self, user):
        sites = Site.objects.order_by('name')

        if not get_cms_setting('PERMISSION') or user.is_superuser:
            return sites
        _has_perm = page_permissions.user_can_change_at_least_one_page
        return [site for site in sites if _has_perm(user, site)]

    def changelist_view(self, request, extra_context=None):
        from django.contrib.admin.views.main import ERROR_FLAG

        if not self.has_change_permission(request, obj=None):
            raise PermissionDenied

        if request.method == 'POST' and 'site' in request.POST:
            site_id = request.POST['site']

            if site_id.isdigit() and Site.objects.filter(pk=site_id).exists():
                request.session['cms_admin_site'] = site_id

        site = self.get_site(request)
        # Language may be present in the GET dictionary but empty
        language = request.GET.get('language', get_language())

        if not language:
            language = get_language()

        query = request.GET.get('q', '')
        pages = self.get_queryset(request)
        pages, use_distinct = self.get_search_results(request, pages, query)

        changelist_form = self.changelist_form(request.GET)

        try:
            changelist_form.full_clean()
            pages = changelist_form.run_filters(pages)
        except (ValueError, ValidationError):
            # Wacky lookup parameters were given, so redirect to the main
            # changelist page, without parameters, and pass an 'invalid=1'
            # parameter via the query string. If wacky parameters were given
            # and the 'invalid=1' parameter was already in the query string,
            # something is screwed up with the database, so display an error
            # page.
            if ERROR_FLAG in request.GET.keys():
                return SimpleTemplateResponse('admin/invalid_setup.html', {
                    'title': _('Database error'),
                })
            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')

        if changelist_form.is_filtered():
            pages = pages.prefetch_related(
                Prefetch(
                    'title_set',
                    to_attr='filtered_translations',
                    queryset=Title.objects.filter(language__in=get_language_list(site.pk))
                ),
            )
            pages = pages.distinct() if use_distinct else pages
            # Evaluates the queryset
            has_items = len(pages) >= 1
        else:
            has_items = pages.exists()

        context = self.admin_site.each_context(request)
        context.update({
            'opts': self.model._meta,
            'media': self.media,
            'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'),
            'CMS_PERMISSION': get_cms_setting('PERMISSION'),
            'site_languages': get_language_list(site.pk),
            'preview_language': language,
            'changelist_form': changelist_form,
            'cms_current_site': site,
            'has_add_permission': self.has_add_permission(request),
            'module_name': force_str(self.model._meta.verbose_name_plural),
            'admin': self,
            'tree': {
                'site': site,
                'sites': self.get_sites_for_user(request.user),
                'query': query,
                'is_filtered': changelist_form.is_filtered(),
                'items': pages,
                'has_items': has_items,
            },
        })
        context.update(extra_context or {})
        request.current_app = self.admin_site.name
        return TemplateResponse(request, self.change_list_template, context)

    @require_POST
    def change_template(self, request, object_id):
        page = self.get_object(request, object_id=object_id)

        if not self.has_change_permission(request, obj=page):
            raise PermissionDenied('No permissions to change the template')

        if page is None:
            raise self._get_404_exception(object_id)

        if not self.has_change_advanced_settings_permission(request, obj=page):
            raise PermissionDenied('No permissions to change the template')

        to_template = request.POST.get("template", None)

        if to_template not in dict(get_cms_setting('TEMPLATES')):
            return HttpResponseBadRequest(force_str(_("Template not valid")))

        page.template = to_template
        page.save()
        return HttpResponse(force_str(_("The template was successfully changed")))

    @require_POST
    @transaction.atomic
    def move_page(self, request, page_id, extra_context=None):
        """
        Move the page to the requested target, at the given position.

        NOTE: We have to change from one "coordinate system" to another to
        adapt JSTree to Django Treebeard.

        If the Tree looks like this:

            <root>
               ⊢ …
               ⊢ …
               ⊢ Page 4
                   ⊢ Page 5 (position 0)
                   ⊢ …

        For example,
            target=4, position=1 => target=5, position="right"
            target=4, position=0 => target=4, position="first-child"

        """
        site = self.get_site(request)
        page = self.get_object(request, object_id=page_id)

        if page is None:
            return jsonify_request(HttpResponseBadRequest("error"))

        user = request.user
        form = self.move_form(request.POST or None, page=page, site=site)

        if not form.is_valid():
            return jsonify_request(HttpResponseBadRequest("error"))

        target = form.cleaned_data['target']
        can_move_page = self.has_move_page_permission(request, obj=page)

        # Does the user have permissions to do this...?
        if not can_move_page or (target and not target.has_add_permission(user)):
            message = force_str(_("Error! You don't have permissions "
                                   "to move this page. Please reload the page"))
            return jsonify_request(HttpResponseForbidden(message))

        operation_token = self._send_pre_page_operation(
            request,
            operation=operations.MOVE_PAGE,
            obj=page,
        )

        form.move_page()

        self._send_post_page_operation(
            request,
            operation=operations.MOVE_PAGE,
            token=operation_token,
            obj=page,
        )
        return jsonify_request(HttpResponse(status=200))

    def get_permissions(self, request, page_id):
        rows = []
        user = request.user
        page = self.get_object(request, object_id=page_id)

        if page is None:
            raise self._get_404_exception(page_id)

        site = self.get_site(request)
        PermissionRow = namedtuple('Permission', ['is_global', 'can_change', 'permission'])

        global_permissions = GlobalPagePermission.objects.filter(sites__in=[site.pk])
        can_change_global_permissions = permissions.user_can_change_global_permissions(user, site)

        for permission in global_permissions.iterator():
            row = PermissionRow(
                is_global=True,
                can_change=can_change_global_permissions,
                permission=permission,
            )
            rows.append(row)

        _page_permissions = (
            PagePermission
            .objects
            .for_page(page)
            .select_related('page')
        )

        if not can_change_global_permissions:
            allowed_pages = frozenset(page_permissions.get_change_id_list(user, site, check_global=False))

        for permission in _page_permissions.iterator():
            if can_change_global_permissions:
                can_change = True
            else:
                can_change = permission.page_id in allowed_pages

            row = PermissionRow(
                is_global=False,
                can_change=can_change,
                permission=permission,
            )
            rows.append(row)

        context = {
            'page': page,
            'rows': rows,
            'opts': self.opts,
        }
        return render(request, 'admin/cms/page/permissions.html', context)

    @require_POST
    @transaction.atomic
    def copy_language(self, request, page_id):
        page = self.get_object(request, object_id=page_id)
        source_language = request.POST.get('source_language')
        target_language = request.POST.get('target_language')

        if not self.has_change_permission(request, obj=page):
            raise PermissionDenied

        if page is None:
            raise self._get_404_exception(page_id)

        if not target_language or target_language not in get_language_list(site_id=page.node.site_id):
            return HttpResponseBadRequest(force_str(_("Language must be set to a supported language!")))

        for placeholder in page.get_placeholders():
            plugins = list(
                placeholder.get_plugins(language=source_language).order_by('path'))
            if not placeholder.has_add_plugins_permission(request.user, plugins):
                return HttpResponseForbidden(force_str(_('You do not have permission to copy these plugins.')))
            copy_plugins.copy_plugins_to(plugins, placeholder, target_language)
        return HttpResponse("ok")

    @require_POST
    @transaction.atomic
    def copy_page(self, request, page_id):
        """
        Copy the page and all its plugins and descendants to the requested
        target, at the given position
        """
        page = self.get_page_from_id(page_id)

        if page is None:
            return jsonify_request(HttpResponseBadRequest("error"))

        user = request.user
        site = self.get_site(request)
        form = self.copy_form(request.POST or None, page=page, site=site)

        if not form.is_valid():
            return jsonify_request(HttpResponseBadRequest("error"))

        target = form.cleaned_data['target']
        source_site = form.cleaned_data['source_site']

        # User can only copy pages he can see
        can_copy_page = page_permissions.user_can_view_page(user, page, source_site)

        if can_copy_page and target:
            # User can only copy a page into another one if he has permission
            # to add a page under the target page.
            can_copy_page = page_permissions.user_can_add_subpage(user, target, site)
        elif can_copy_page:
            # User can only copy / paste a page if he has permission to add a page
            can_copy_page = page_permissions.user_can_add_page(user, site)

        if not can_copy_page:
            message = force_str(_("Error! You don't have permissions to copy this page."))
            return jsonify_request(HttpResponseForbidden(message))

        page_languages = page.get_languages()
        site_languages = get_language_list(site_id=site.pk)

        if not any(lang in  page_languages for lang in site_languages):
            message = force_str(_("Error! The page you're pasting is not "
                                   "translated in any of the languages configured by the target site."))
            return jsonify_request(HttpResponseBadRequest(message))

        new_page = form.copy_page()
        return HttpResponse(json.dumps({"id": new_page.pk}), content_type='application/json')

    @require_POST
    @transaction.atomic
    def revert_to_live(self, request, page_id, language):
        """
        Resets the draft version of the page to match the live one
        """
        page = self.get_object(request, object_id=page_id)

        if not self.has_revert_to_live_permission(request, language, obj=page):
            return HttpResponseForbidden(force_str(_("You do not have permission to revert this page.")))

        if page is None:
            raise self._get_404_exception(page_id)

        try:
            translation = page.title_set.get(language=language)
        except Title.DoesNotExist:
            raise Http404('No translation matches requested language.')

        page.title_cache[language] = translation
        operation_token = self._send_pre_page_operation(
            request,
            operation=operations.REVERT_PAGE_TRANSLATION_TO_LIVE,
            obj=page,
            translation=translation,
        )

        page.revert_to_live(language)

        # Fetch updated translation
        translation.refresh_from_db()

        self._send_post_page_operation(
            request,
            operation=operations.REVERT_PAGE_TRANSLATION_TO_LIVE,
            token=operation_token,
            obj=page,
            translation=translation,
        )

        messages.info(request, _('"%s" was reverted to the live version.') % page)

        path = page.get_absolute_url(language=language)
        path = '{}?{}'.format(path, get_cms_setting('CMS_TOOLBAR_URL__EDIT_OFF'))
        return HttpResponseRedirect(path)

    @require_POST
    @transaction.atomic
    def publish_page(self, request, page_id, language):
        page = self.get_object(request, object_id=page_id)

        if page and not self.has_publish_permission(request, obj=page):
            return HttpResponseForbidden(force_str(_("You do not have permission to publish this page")))

        if page:
            translation = page.get_title_obj(language, fallback=False)
        else:
            translation = None

        if page and not translation:
            raise Http404('No translation matches requested language.')

        all_published = True

        statics = request.GET.get('statics', '')

        if not statics and not page:
            raise Http404("No page or static placeholder found for publishing.")

        if translation and translation.publisher_public:
            reload_urls = translation._url_properties_changed()
        else:
            reload_urls = bool(page and page.application_urls and translation)

        if page:
            operation_token = self._send_pre_page_operation(
                request,
                operation=operations.PUBLISH_PAGE_TRANSLATION,
                obj=page,
                translation=translation,
            )
            all_published = page.publish(language)
            page = page.reload()
            self._send_post_page_operation(
                request,
                operation=operations.PUBLISH_PAGE_TRANSLATION,
                token=operation_token,
                obj=page,
                translation=translation,
                successful=all_published,
            )

        if statics:
            static_ids = statics.split(',')
            static_placeholders = StaticPlaceholder.objects.filter(pk__in=static_ids)

            for static_placeholder in static_placeholders.iterator():
                # TODO: Maybe only send one signal...
                # this would break the obj signal format though
                operation_token = self._send_pre_page_operation(
                    request,
                    operation=operations.PUBLISH_STATIC_PLACEHOLDER,
                    obj=static_placeholder,
                    target_language=language,
                )

                published = static_placeholder.publish(request, language)

                self._send_post_page_operation(
                    request,
                    operation=operations.PUBLISH_STATIC_PLACEHOLDER,
                    token=operation_token,
                    obj=static_placeholder,
                    target_language=language,
                )
                if not published:
                    all_published = False

        if page:
            if all_published:
                if reload_urls:
                    set_restart_trigger()

                if page.get_publisher_state(language) == PUBLISHER_STATE_PENDING:
                    messages.warning(request, _("Page not published! A parent page is not published yet."))
                else:
                    messages.info(request, _('The content was successfully published.'))
                LogEntry.objects.log_action(
                    user_id=request.user.id,
                    content_type_id=ContentType.objects.get_for_model(Page).pk,
                    object_id=page_id,
                    object_repr=page.get_title(language),
                    action_flag=CHANGE,
                )
            else:
                if page.get_publisher_state(language) == PUBLISHER_STATE_PENDING:
                    messages.warning(request, _("Page not published! A parent page is not published yet."))
                else:
                    messages.warning(request, _("There was a problem publishing your content"))

        if 'node' in request.GET or 'node' in request.POST:
            # if request comes from tree..
            # 204 -> request was successful but no response returned.
            return HttpResponse(status=204)

        if 'redirect' in request.GET:
            return HttpResponseRedirect(request.GET['redirect'])
        referrer = request.headers.get('Referer', '')

        path = admin_reverse("cms_page_changelist")
        if request.GET.get('redirect_language'):
            path = "{}?language={}&page_id={}".format(path, request.GET.get('redirect_language'), request.GET.get('redirect_page_id'))
        if admin_reverse('index') not in referrer:
            if all_published:
                if page:
                    if page.get_publisher_state(language) == PUBLISHER_STATE_PENDING:
                        path = page.get_absolute_url(language, fallback=True)
                    else:
                        public_page = Page.objects.get(publisher_public=page.pk)
                        path = '{}?preview&{}'.format(public_page.get_absolute_url(language, fallback=True), get_cms_setting('CMS_TOOLBAR_URL__EDIT_OFF'))
                else:
                    path = '{}?preview&{}'.format(referrer, get_cms_setting('CMS_TOOLBAR_URL__EDIT_OFF'))
            else:
                path = '/?preview&%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_OFF')

        return HttpResponseRedirect(path)

    @require_POST
    @transaction.atomic
    def unpublish(self, request, page_id, language):
        """
        Publish or unpublish a language of a page
        """
        page = self.get_object(request, object_id=page_id)

        if not self.has_publish_permission(request, obj=page):
            return HttpResponseForbidden(force_str(_("You do not have permission to unpublish this page")))

        if page is None:
            raise self._get_404_exception(page_id)

        if not page.publisher_public_id:
            return HttpResponseBadRequest(force_str(_("This page was never published")))

        has_translation = page.publisher_public.title_set.filter(language=language).exists()

        if not has_translation:
            raise Http404('No translation matches requested language.')

        language_name = get_language_object(language, site_id=page.node.site_id)['name']

        try:
            page.unpublish(language)
            message = _('The %(language)s page "%(page)s" was successfully unpublished') % {
                'language': language_name, 'page': page}
            messages.info(request, message)
            LogEntry.objects.log_action(
                user_id=request.user.id,
                content_type_id=ContentType.objects.get_for_model(Page).pk,
                object_id=page_id,
                object_repr=page.get_title(),
                action_flag=CHANGE,
                change_message=message,
            )
        except RuntimeError:
            exc = sys.exc_info()[1]
            messages.error(request, exc.message)
        except ValidationError:
            exc = sys.exc_info()[1]
            messages.error(request, exc.message)
        path = admin_reverse("cms_page_changelist")
        if request.GET.get('redirect_language'):
            path = "{}?language={}&page_id={}".format(path, request.GET.get('redirect_language'), request.GET.get('redirect_page_id'))
        return HttpResponseRedirect(path)

    def delete_translation(self, request, object_id, extra_context=None):
        if 'delete_language' in request.GET:
            language = request.GET['delete_language']
        elif 'delete_language' in request.POST:
            language = request.POST['delete_language']
        else:
            language = get_language_from_request(request)

        page, translation = self.get_object_with_translation(
            request=request,
            object_id=object_id,
            language=language,
        )

        if not self.has_delete_translation_permission(request, language, page):
            return HttpResponseForbidden(force_str(_("You do not have permission to delete this page")))

        if page is None:
            raise self._get_404_exception(object_id)

        if not len(list(page.get_languages())) > 1:
            return HttpResponseBadRequest('There only exists one translation for this page')

        if translation is None:
            raise Http404('No translation matches requested language.')

        titleopts = Title._meta
        app_label = titleopts.app_label
        CMSPlugin._meta

        saved_plugins = CMSPlugin.objects.filter(placeholder__page__id=object_id, language=language)
        router.db_for_read(self.model)

        kwargs = {'admin_site': self.admin_site}
        kwargs.update({'request': request})
        deleted_objects, __, perms_needed = get_deleted_objects(
            [translation],
            **kwargs
        )[:3]

        kwargs.update({'request': request})
        to_delete_plugins, __, perms_needed_plugins = get_deleted_objects(
            saved_plugins,
            **kwargs
        )[:3]

        deleted_objects.append(to_delete_plugins)
        perms_needed = set(list(perms_needed) + list(perms_needed_plugins))

        if request.method == 'POST':
            if perms_needed:
                raise PermissionDenied

            operation_token = self._send_pre_page_operation(
                request,
                operation=operations.DELETE_PAGE_TRANSLATION,
                obj=page,
                translation=translation,
            )

            message = _('Title and plugins with language %(language)s was deleted') % {
                'language': force_str(get_language_object(language)['name'])
            }
            self.log_change(request, translation, message)
            messages.success(request, message)

            translation.delete()
            for p in saved_plugins:
                p.delete()

            page.remove_language(language)

            if page.node.is_branch:
                page.mark_descendants_pending(language)

            self._send_post_page_operation(
                request,
                operation=operations.DELETE_PAGE_TRANSLATION,
                token=operation_token,
                obj=page,
                translation=translation,
            )

            if not self.has_change_permission(request, None):
                return HttpResponseRedirect(admin_reverse('index'))
            return HttpResponseRedirect(self.get_admin_url('changelist'))

        context = {
            "title": _("Are you sure?"),
            "object_name": force_str(titleopts.verbose_name),
            "object": translation,
            "deleted_objects": deleted_objects,
            "perms_lacking": perms_needed,
            "opts": self.opts,
            "root_path": admin_reverse('index'),
            "app_label": app_label,
        }
        context.update(extra_context or {})
        request.current_app = self.admin_site.name
        return render(request, self.delete_confirmation_template or [
            f"admin/{app_label}/{titleopts.object_name.lower()}/delete_confirmation.html",
            "admin/%s/delete_confirmation.html" % app_label,
            "admin/delete_confirmation.html"
        ], context)

    def preview_page(self, request, object_id, language):
        """
        Redirecting preview function based on draft_id
        """
        page = self.get_object(request, object_id=object_id)

        if page is None:
            raise self._get_404_exception(object_id)

        site = get_current_site()
        active_site = self.get_site(request)
        can_see_page = page_permissions.user_can_view_page(request.user, page, active_site)

        if can_see_page:
            can_change_page = self.has_change_permission(request, obj=page)
        else:
            can_change_page = False

        if can_see_page and not can_change_page:
            # User can see the page but has no permission to edit it,
            # as a result, only let them see it if is published.
            can_see_page = page.is_published(language)

        if not can_see_page:
            message = gettext('You don\'t have permissions to see page "%(title)s"')
            message = message % {'title': force_str(page)}
            self.message_user(request, message, level=messages.ERROR)
            return HttpResponseRedirect(self.get_admin_url('changelist'))

        attrs = "?%s" % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
        attrs += "&language=" + language
        url = page.get_absolute_url(language) + attrs

        if site != active_site and page.node.site_id != site.pk:
            # The user has selected a site using the site selector menu
            # and the page is not available on the current site's tree.
            # Redirect to the page url in the selected site
            proto = 'https' if request.is_secure() else 'http'
            url = f"{proto}://{active_site.domain}{url}"
        return HttpResponseRedirect(url)

    @require_POST
    def change_innavigation(self, request, page_id):
        """
        Switch the in_navigation of a page
        """
        page = self.get_object(request, object_id=page_id)

        if not self.has_change_permission(request, obj=page):
            message = _("You do not have permission to change this page's in_navigation status")
            return HttpResponseForbidden(force_str(message))

        if page is None:
            raise self._get_404_exception(page_id)

        page.toggle_in_navigation()
        # 204 -> request was successful but no response returned.
        return HttpResponse(status=204)

    def get_tree(self, request):
        """
        Get html for the descendants (only) of given page or if no page_id is
        provided, all the root nodes.

        Used for lazy loading pages in cms.pagetree.js
        """
        site = self.get_site(request)
        pages = self.get_queryset(request)
        node_id = re.sub(r'[^\d]', '', request.GET.get('nodeId', '')) or None
        open_nodes = list(map(
            int,
            [re.sub(r'[^\d]', '', node) for node in
                request.GET.getlist('openNodes[]')]
            ))

        if node_id:
            page = get_object_or_404(pages, node_id=int(node_id))
            pages = page.get_descendant_pages().filter(Q(node__in=open_nodes) |Q(node__parent__in=open_nodes))
        else:
            page = None
            pages = pages.filter(
                # get all root nodes
                Q(node__depth=1)
                # or children which were previously open
                | Q(node__depth=2, node__in=open_nodes)  # noqa: W503
                # or children of the open descendants
                | Q(node__parent__in=open_nodes)  # noqa: W503
            )
        pages = pages.prefetch_related(
            Prefetch(
                'title_set',
                to_attr='filtered_translations',
                queryset=Title.objects.filter(language__in=get_language_list(site.pk))
            ),
        )
        rows = self.get_tree_rows(
            request,
            pages=pages,
            language=get_site_language_from_request(request, site_id=site.pk),
            depth=(page.node.depth + 1 if page else 1),
            follow_descendants=True,
        )
        return HttpResponse(''.join(rows))

    def get_tree_rows(self, request, pages, language, depth=1,
                      follow_descendants=True):
        """
        Used for rendering the page tree, inserts into context everything what
        we need for single item
        """
        user = request.user
        site = self.get_site(request)
        permissions_on = get_cms_setting('PERMISSION')
        template = get_template(self.page_tree_row_template)
        is_popup = (IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET)
        languages = get_language_list(site.pk)
        user_can_add = page_permissions.user_can_add_subpage

        def render_page_row(page):
            page.title_cache = {trans.language: trans for trans in page.filtered_translations}

            for _language in languages:
                # EmptyTitle is used to prevent the cms from trying
                # to find a translation in the database
                page.title_cache.setdefault(_language, EmptyTitle(language=_language))

            has_move_page_permission = self.has_move_page_permission(request, obj=page)

            if permissions_on and not has_move_page_permission:
                # TODO: check if this is really needed
                metadata = '{"valid_children": False, "draggable": False}'
            else:
                metadata = ''

            context = {
                'admin': self,
                'opts': self.opts,
                'site': site,
                'page': page,
                'node': page.node,
                'ancestors': [node.item for node in page.node.get_cached_ancestors()],
                'descendants': [node.item for node in page.node.get_cached_descendants()],
                'request': request,
                'lang': language,
                'metadata': metadata,
                'page_languages': page.get_languages(),
                'preview_language': language,
                'follow_descendants': follow_descendants,
                'site_languages': languages,
                'is_popup': is_popup,
                'has_add_page_permission': user_can_add(user, target=page),
                'has_change_permission': self.has_change_permission(request, obj=page),
                'has_publish_permission':  self.has_publish_permission(request, obj=page),
                'has_change_advanced_settings_permission': self.has_change_advanced_settings_permission(request, obj=page),
                'has_move_page_permission': has_move_page_permission,
            }
            return template.render(context)

        if follow_descendants:
            root_pages = (page for page in pages if page.node.depth == depth)
        else:
            # When the tree is filtered, it's displayed as a flat structure
            root_pages = pages

        if depth == 1:
            nodes = []

            for page in pages:
                page.node.__dict__['item'] = page
                nodes.append(page.node)

            for page in root_pages:
                page.node._set_hierarchy(nodes)
                yield render_page_row(page)
        else:
            for page in root_pages:
                page.node.__dict__['item'] = page
                yield render_page_row(page)

    def resolve(self, request):
        if not request.user.is_staff:
            return HttpResponse('/', content_type='text/plain')
        obj = False
        url = False
        if request.session.get('cms_log_latest', False):
            log = LogEntry.objects.get(pk=request.session['cms_log_latest'])
            try:
                obj = log.get_edited_object()
            except (ObjectDoesNotExist, ValueError):
                obj = None
            del request.session['cms_log_latest']
            if obj and obj.__class__ in toolbar_pool.get_watch_models() and hasattr(obj, 'get_absolute_url'):
                # This is a test if the object url can be retrieved
                # In case it can't, object it's not taken into account
                try:
                    force_str(obj.get_absolute_url())
                except [AttributeError, NoReverseMatch, TypeError]:
                    obj = None
            else:
                obj = None
        if not obj:
            pk = request.GET.get('pk', False) or request.POST.get('pk', False)
            full_model = request.GET.get('model') or request.POST.get('model', False)
            if pk and full_model:
                app_label, model = full_model.split('.')
                if pk and app_label:
                    ctype = ContentType.objects.get(app_label=app_label, model=model)
                    try:
                        obj = ctype.get_object_for_this_type(pk=pk)
                    except ctype.model_class().DoesNotExist:
                        obj = None
                    try:
                        force_str(obj.get_absolute_url())
                    except:  # noqa: E722
                        obj = None
        if obj:
            if not getattr(request, 'toolbar', False) or not getattr(request.toolbar, 'edit_mode_active', False):
                if isinstance(obj, Page):
                    if obj.get_public_object():
                        url = obj.get_public_object().get_absolute_url()
                    else:
                        url = '{}?{}'.format(
                            obj.get_draft_object().get_absolute_url(),
                            get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
                        )
                else:
                    url = obj.get_absolute_url()
            else:
                url = obj.get_absolute_url()
        if url:
            return HttpResponse(force_str(url), content_type='text/plain')
        return HttpResponse('', content_type='text/plain')

    def edit_title_fields(self, request, page_id, language):
        page, translation = self.get_object_with_translation(
            request=request,
            object_id=page_id,
            language=language,
        )

        if not self.has_change_permission(request, obj=page):
            return HttpResponseForbidden(force_str(_("You do not have permission to edit this page")))

        if page is None:
            raise self._get_404_exception(page_id)

        if translation is None:
            raise Http404('No translation matches requested language.')

        saved_successfully = False
        raw_fields = request.GET.get("edit_fields", 'title')
        edit_fields = [field for field in raw_fields.split(",") if field in self.title_frontend_editable_fields]
        cancel_clicked = request.POST.get("_cancel", False)
        opts = Title._meta

        if not edit_fields:
            # Defaults to title
            edit_fields = ('title',)

        class PageTitleForm(django.forms.ModelForm):
            """
            Dynamic form showing only the fields to be edited
            """
            class Meta:
                model = Title
                fields = edit_fields

        if not cancel_clicked and request.method == 'POST':
            form = PageTitleForm(instance=translation, data=request.POST)
            if form.is_valid():
                form.save()
                saved_successfully = True
        else:
            form = PageTitleForm(instance=translation)
        admin_form = AdminForm(form, fieldsets=[(None, {'fields': edit_fields})], prepopulated_fields={},
                               model_admin=self)
        media = self.media + admin_form.media
        context = {
            'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'),
            'title': 'Title',
            'plugin': page,
            'plugin_id': page.pk,
            'adminform': admin_form,
            'add': False,
            'is_popup': True,
            'media': media,
            'opts': opts,
            'change': True,
            'save_as': False,
            'has_add_permission': False,
            'window_close_timeout': 10,
        }
        if cancel_clicked:
            # cancel button was clicked
            context.update({
                'cancel': True,
            })
            return render(request, 'admin/cms/page/plugin/confirm_form.html', context)
        if not cancel_clicked and request.method == 'POST' and saved_successfully:
            return render(request, 'admin/cms/page/plugin/confirm_form.html', context)
        return render(request, 'admin/cms/page/plugin/change_form.html', context)


class PageAdmin(BasePageAdmin):

    def add_view(self, request, form_url='', extra_context=None):
        if extra_context is None:
            extra_context = {}

        if 'duplicate' in request.path_info:
            extra_context.update({
                'title':  _("Add Page Copy"),
            })
        elif 'parent_node' in request.GET:
            extra_context.update({
                'title':  _("New sub page"),
            })
        else:
            extra_context = self.update_language_tab_context(request, context=extra_context)
            extra_context.update({
                'title':  _("New page"),
            })
        return super().add_view(request, form_url, extra_context=extra_context)

    def get_queryset(self, request):
        queryset  = super().get_queryset(request)
        return queryset.exclude(is_page_type=True)

    def get_urls(self):
        """
        Get the admin urls
        """
        info = f"{self.model._meta.app_label}_{self.model._meta.model_name}"

        def pat(regex, fn):
            return re_path(regex, self.admin_site.admin_view(fn), name=f'{info}_{fn.__name__}')

        url_patterns = [
            pat(r'^([0-9]+)/set-home/$', self.set_home),
            pat(r'^published-pages/$', self.get_published_pagelist),
            re_path(r'^resolve/$', self.resolve, name="cms_page_resolve"),
        ]

        if plugin_pool.registered_plugins:
            url_patterns += plugin_pool.get_patterns()
        return url_patterns + super().get_urls()

    @require_POST
    @transaction.atomic
    def set_home(self, request, object_id):
        page = self.get_object(request, object_id=object_id)

        if not self.has_change_permission(request, page):
            raise PermissionDenied("You do not have permission to set 'home'.")

        if page is None:
            raise self._get_404_exception(object_id)

        if not page.is_potential_home():
            return HttpResponseBadRequest(force_str(_("The page is not eligible to be home.")))

        new_home_tree, old_home_tree = page.set_as_homepage(request.user)

        # Check if one of the affected pages either from the old homepage
        # or the homepage had an apphook attached
        if old_home_tree:
            apphooks_affected = old_home_tree.filter(publisher_is_draft=False).has_apphooks()
        else:
            apphooks_affected = False

        if not apphooks_affected:
            apphooks_affected = new_home_tree.filter(publisher_is_draft=False).has_apphooks()

        if apphooks_affected:
            # One or more pages affected by this operation was attached to an apphook.
            # As a result, fire the apphook reload signal to reload the url patterns.
            set_restart_trigger()
        return HttpResponse('ok')

    def get_published_pagelist(self, *args, **kwargs):
        """
         This view is used by the PageSmartLinkWidget as the user type to feed the autocomplete drop-down.
        """
        request = args[0]

        if request.headers.get('x-requested-with') == 'XMLHttpRequest':
            query_term = request.GET.get('q','').strip('/')

            language_code = request.GET.get('language_code', settings.LANGUAGE_CODE)
            matching_published_pages = self.model.objects.published().public().filter(
                Q(
                    title_set__title__icontains=query_term, title_set__language=language_code
                ) | Q(
                    title_set__path__icontains=query_term, title_set__language=language_code
                ) | Q(
                    title_set__menu_title__icontains=query_term, title_set__language=language_code
                ) | Q(
                    title_set__page_title__icontains=query_term, title_set__language=language_code
                )
            ).distinct()

            results = []
            for page in matching_published_pages:
                results.append(
                    {
                        'path': page.get_path(language=language_code),
                        'title': page.get_title(language=language_code),
                        'redirect_url': page.get_absolute_url(language=language_code)
                    }
                )
            return HttpResponse(json.dumps(results), content_type='application/json')
        return HttpResponseForbidden()


class PageTypeAdmin(BasePageAdmin):

    add_form = AddPageTypeForm
    change_form_template = 'admin/cms/page/change_form.html'

    def get_queryset(self, request):
        queryset  = super().get_queryset(request)
        return queryset.exclude(is_page_type=False)


admin.site.register(Page, PageAdmin)
admin.site.register(PageType, PageTypeAdmin)