forked from ungleich-public/dynamicweb
1513 lines
60 KiB
Python
1513 lines
60 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from collections import defaultdict, 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.options import IS_POPUP_VAR
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import (
|
|
PermissionDenied,
|
|
ValidationError,
|
|
)
|
|
from django.db import transaction
|
|
from django.db.models import Prefetch, Q
|
|
from django.db.models.query import QuerySet
|
|
from django.forms import ModelForm
|
|
from django.forms.fields import IntegerField
|
|
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 re_path
|
|
from django.utils.decorators import method_decorator
|
|
from django.utils.encoding import force_str
|
|
from django.utils.html import format_html
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.text import capfirst
|
|
from django.utils.translation import gettext as _
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from cms import operations
|
|
from cms.admin.forms import (
|
|
AddPageForm,
|
|
AdvancedSettingsForm,
|
|
ChangeListForm,
|
|
ChangePageForm,
|
|
CopyPageForm,
|
|
CopyPermissionForm,
|
|
DuplicatePageForm,
|
|
MovePageForm,
|
|
)
|
|
from cms.admin.permissionadmin import PERMISSION_ADMIN_INLINES
|
|
from cms.cache.permissions import clear_permission_cache
|
|
from cms.constants import MODAL_HTML_REDIRECT
|
|
from cms.models import (
|
|
CMSPlugin,
|
|
GlobalPagePermission,
|
|
Page,
|
|
PageContent,
|
|
PagePermission,
|
|
PageUrl,
|
|
Placeholder,
|
|
)
|
|
from cms.models.pagemodel import AdminCacheDict
|
|
from cms.models.permissionmodels import PermissionTuple
|
|
from cms.operations.helpers import (
|
|
send_post_page_operation,
|
|
send_pre_page_operation,
|
|
)
|
|
from cms.plugin_pool import plugin_pool
|
|
from cms.signals.apphook import set_restart_trigger
|
|
from cms.toolbar.utils import get_object_edit_url
|
|
from cms.utils import get_current_site, page_permissions, permissions
|
|
from cms.utils.admin import get_site_from_request, jsonify_request
|
|
from cms.utils.compat.warnings import RemovedInDjangoCMS60Warning
|
|
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.permissions import clear_permission_lru_caches
|
|
from cms.utils.plugins import copy_plugins_to_placeholder, downcast_plugins
|
|
from cms.utils.urlutils import admin_reverse, static_with_version
|
|
|
|
require_POST = method_decorator(require_POST)
|
|
|
|
#: Upper bound for the number of pages :meth:`PageAdmin.get_list` returns. The
|
|
#: view feeds an autocomplete, so a full inventory of the site is never useful.
|
|
PAGE_SMART_LINK_MAX_RESULTS = 50
|
|
|
|
|
|
class PageDeleteMessageMixin:
|
|
"""Expressive and simplified delete confirmation message for pages and translations."""
|
|
|
|
delete_confirmation_template = "admin/cms/page/delete_confirmation.html"
|
|
|
|
def get_deleted_objects(self, objs, request):
|
|
"""Minimize complexity of delete selected confirmation: Only show pages, page contents and plugins numbers,
|
|
only show Page and PageContent objects in the details.
|
|
"""
|
|
|
|
def recursively_remove(deleted_objects: list | str) -> list | str:
|
|
"""Remove all objects that are not Page or PageContent from the nested list of deleted objects.
|
|
Reformat the messages."""
|
|
if isinstance(deleted_objects, str):
|
|
return deleted_objects
|
|
result = []
|
|
for obj in deleted_objects:
|
|
item = recursively_remove(obj)
|
|
if isinstance(item, str):
|
|
if obj.startswith(f"{capfirst(Page._meta.verbose_name)}: "):
|
|
# ``Page`` is registered in the admin, so Django built this entry with
|
|
# ``format_html()`` as ``Page: <a href="...">title</a>``: the title inside the
|
|
# anchor is already escaped and must not be escaped a second time.
|
|
text = re.findall(r">(.*)<", obj)
|
|
if text:
|
|
result.append(format_html("<b>{}</b>", mark_safe(text[0])))
|
|
else:
|
|
# No admin link, so this is Django's raw ``"Page: %s" % obj`` string.
|
|
result.append(
|
|
format_html("<b>{}</b>", item.removeprefix(f"{capfirst(Page._meta.verbose_name)}: "))
|
|
)
|
|
elif obj.startswith(f"{capfirst(PageUrl._meta.verbose_name)}: "):
|
|
# ``PageUrl`` has no admin, so Django returns the raw, unescaped
|
|
# ``"Page url: %s" % obj`` string. The path is author-controlled (it can be set
|
|
# through "Overwrite URL"), so it must not be marked safe (CWE-79).
|
|
result.insert(0, escape(item.removeprefix(f"{capfirst(PageUrl._meta.verbose_name)}: ")))
|
|
elif item:
|
|
result.append(item)
|
|
return result
|
|
|
|
if len(objs) == 1 and isinstance(objs[0], PageContent):
|
|
# Always look for all page/language combinations
|
|
objs = list(PageContent.admin_manager.filter(page=objs[0].page, language=objs[0].language))
|
|
|
|
to_delete, trad_model_count, perms_needed, protected = super().get_deleted_objects(objs, request)
|
|
to_delete = recursively_remove(to_delete)
|
|
|
|
model_count = {
|
|
Page._meta.verbose_name_plural: trad_model_count.get(Page._meta.verbose_name_plural, 0),
|
|
_("Translations"): trad_model_count.get(PageUrl._meta.verbose_name_plural, 0),
|
|
CMSPlugin._meta.verbose_name_plural: trad_model_count.get(CMSPlugin._meta.verbose_name_plural, 0),
|
|
}
|
|
return to_delete, model_count, perms_needed, protected
|
|
|
|
|
|
@admin.register(Page)
|
|
class PageAdmin(PageDeleteMessageMixin, admin.ModelAdmin):
|
|
actions_menu_template = "admin/cms/page/tree/actions_dropdown.html"
|
|
|
|
form = AdvancedSettingsForm
|
|
copy_form = CopyPageForm
|
|
move_form = MovePageForm
|
|
inlines = PERMISSION_ADMIN_INLINES
|
|
search_fields = ("=id", "urls__slug", "pagecontent_set__title", "reverse_id")
|
|
title_frontend_editable_fields = ["title", "menu_title", "page_title"]
|
|
|
|
def has_module_permission(self, request):
|
|
return False # Hides page model from the admin index
|
|
|
|
def has_add_permission(self, request):
|
|
return False
|
|
|
|
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.
|
|
"""
|
|
if obj is None:
|
|
site = get_site_from_request(request)
|
|
# Checks if user can change at least one page
|
|
return page_permissions.user_can_change_at_least_one_page(
|
|
user=request.user,
|
|
site=site,
|
|
)
|
|
return page_permissions.user_can_change_page(request.user, page=obj, site=obj.site)
|
|
|
|
def has_change_advanced_settings_permission(self, request, obj=None):
|
|
if not obj:
|
|
return False
|
|
return page_permissions.user_can_change_page_advanced_settings(request.user, page=obj, site=obj.site)
|
|
|
|
def log_deletion(self, request, object, object_repr):
|
|
# DJANGO_42
|
|
# Block the admin log for deletion. A signal takes care of this!
|
|
return
|
|
|
|
def log_deletions(self, request, queryset):
|
|
# Block the admin log for deletion. A signal takes care of this!
|
|
return
|
|
|
|
def get_admin_url(self, action, *args):
|
|
url_name = f"{self.opts.app_label}_{self.opts.model_name}_{action}"
|
|
return admin_reverse(url_name, args=args)
|
|
|
|
def get_preserved_filters(self, request):
|
|
"""
|
|
This override is in place to preserve the "language" get parameter in
|
|
the "Save" page redirect
|
|
"""
|
|
site = get_site_from_request(request)
|
|
preserved_filters_encoded = super().get_preserved_filters(request)
|
|
preserved_filters = QueryDict(preserved_filters_encoded).copy()
|
|
lang = get_site_language_from_request(request, site_id=site.pk)
|
|
|
|
if lang:
|
|
preserved_filters["language"] = lang
|
|
return preserved_filters.urlencode()
|
|
|
|
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)
|
|
except self.model.DoesNotExist:
|
|
page = None
|
|
return page
|
|
|
|
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"^list/$", self.get_list),
|
|
pat(r"^(?P<object_id>[0-9]+)/actions-menu/$", self.actions_menu),
|
|
pat(r"^([0-9]+)/([a-z\-]+)/edit-field/$", self.edit_title_fields),
|
|
pat(r"^(?P<object_id>[0-9]+)/advanced-settings/$", self.advanced), # named object_id
|
|
pat(r"^([0-9]+)/move-page/$", self.move_page),
|
|
pat(r"^([0-9]+)/copy-page/$", self.copy_page),
|
|
pat(r"^([0-9]+)/dialog/copy/$", self.get_copy_dialog), # copy dialog
|
|
pat(r"^([0-9]+)/permissions/$", self.get_permissions),
|
|
pat(r"^(?P<object_id>[0-9]+)/set-home/$", self.set_home),
|
|
]
|
|
|
|
if plugin_pool.registered_plugins:
|
|
url_patterns += plugin_pool.get_patterns()
|
|
return url_patterns + super().get_urls()
|
|
|
|
def get_inline_instances(self, request, obj=None):
|
|
if obj and get_cms_setting("PERMISSION"):
|
|
can_change_perms = self.has_change_permissions_permission(request, obj=obj)
|
|
else:
|
|
can_change_perms = False
|
|
|
|
if can_change_perms:
|
|
return super().get_inline_instances(request, obj)
|
|
return []
|
|
|
|
def get_form(self, request, obj=None, **kwargs):
|
|
"""
|
|
Get PageForm for the Page model and modify its fields depending on
|
|
the request.
|
|
"""
|
|
if not self.has_change_advanced_settings_permission(request, obj=obj):
|
|
form = type("EmptyForm", (ModelForm,), {"Meta": type("Meta", (self.form.Meta,), {"fields": []})})
|
|
else:
|
|
form = super().get_form(request, obj, **kwargs)
|
|
form._site = get_site_from_request(request)
|
|
form._request = request
|
|
return form
|
|
|
|
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)
|
|
|
|
if not self.has_view_permission(request, obj=page):
|
|
raise PermissionDenied("No permission for actions menu")
|
|
|
|
site = get_site_from_request(request)
|
|
paste_enabled = request.GET.get("has_copy") or request.GET.get("has_cut")
|
|
context = {
|
|
"page": page,
|
|
"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 advanced(self, request, object_id):
|
|
page = self.get_object(request, object_id=object_id)
|
|
|
|
if page is None:
|
|
raise self._get_404_exception(object_id)
|
|
|
|
if get_cms_setting("PERMISSION"):
|
|
show_permissions = self.has_change_permissions_permission(request, obj=page)
|
|
else:
|
|
show_permissions = False
|
|
|
|
show_advanced_settings = self.has_change_advanced_settings_permission(request, obj=page)
|
|
|
|
if not show_permissions and not show_advanced_settings:
|
|
raise PermissionDenied("No permission for editing advanced settings")
|
|
|
|
context = {
|
|
"title": _("Advanced Settings") if show_advanced_settings else _("Change Permissions"),
|
|
"show_permissions": show_permissions,
|
|
}
|
|
return self.change_view(request, object_id, extra_context=context)
|
|
|
|
def response_post_save_change(self, request, obj):
|
|
"""
|
|
Figure out where to redirect after the 'Save' button has been pressed
|
|
when adding a new object.
|
|
"""
|
|
can_change_any_page = page_permissions.user_can_change_at_least_one_page(
|
|
user=request.user,
|
|
site=obj.site,
|
|
use_cache=False,
|
|
)
|
|
|
|
if can_change_any_page:
|
|
query = self.get_preserved_filters(request)
|
|
post_url = admin_reverse("cms_pagecontent_changelist") + "?" + query + "&site=" + str(obj.site_id)
|
|
else:
|
|
post_url = admin_reverse("index")
|
|
return HttpResponseRedirect(post_url)
|
|
|
|
@require_POST
|
|
@transaction.atomic
|
|
def set_home(self, request, object_id):
|
|
page = self.get_object(request, object_id=object_id)
|
|
|
|
if page is None:
|
|
raise self._get_404_exception(object_id)
|
|
|
|
if not self.has_change_permission(request, page):
|
|
raise PermissionDenied("You do not have permission to set 'home'.")
|
|
|
|
if not page.is_potential_home():
|
|
return HttpResponseBadRequest(_("The page is not eligible to be home."))
|
|
|
|
# Setting a new home page demotes the current one and rewrites the url
|
|
# paths of both trees, so it takes change permission on both pages.
|
|
old_home = self.model.objects.filter(is_home=True, site=page.site_id).first()
|
|
|
|
if old_home and not self.has_change_permission(request, old_home):
|
|
raise PermissionDenied("You do not have permission to change the current home page.")
|
|
|
|
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.has_apphooks()
|
|
else:
|
|
apphooks_affected = False
|
|
|
|
if not apphooks_affected:
|
|
apphooks_affected = new_home_tree.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_list(self, request):
|
|
"""
|
|
This view is used by the PageSmartLinkWidget as the user type to feed the autocomplete drop-down.
|
|
"""
|
|
|
|
if not self.has_view_permission(request):
|
|
raise PermissionDenied("No permission for page list view")
|
|
|
|
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
|
site = get_site_from_request(request)
|
|
query_term = request.GET.get("q", "").strip("/")
|
|
|
|
if not query_term:
|
|
# ``icontains`` matches every page for an empty term, which would
|
|
# turn this autocomplete into a full listing of the site.
|
|
return HttpResponse(json.dumps([]), content_type="application/json")
|
|
|
|
language_code = request.GET.get("language_code", settings.LANGUAGE_CODE)
|
|
matching_pages = (
|
|
self.model.objects.on_site(site)
|
|
.filter(
|
|
Q(pagecontent_set__title__icontains=query_term, pagecontent_set__language=language_code)
|
|
| Q(urls__path__icontains=query_term, pagecontent_set__language=language_code)
|
|
| Q(pagecontent_set__menu_title__icontains=query_term, pagecontent_set__language=language_code)
|
|
| Q(pagecontent_set__page_title__icontains=query_term, pagecontent_set__language=language_code)
|
|
)
|
|
# Page types are blueprints for new pages, not link targets.
|
|
.filter(is_page_type=False)
|
|
.prefetch_related("urls", "pagecontent_set")
|
|
.distinct()[:PAGE_SMART_LINK_MAX_RESULTS]
|
|
)
|
|
|
|
results = []
|
|
for page in matching_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()
|
|
|
|
def changelist_view(self, request, extra_context=None):
|
|
parameter = "?" + request.GET.urlencode() if request.GET else ""
|
|
return HttpResponseRedirect(admin_reverse("cms_pagecontent_changelist") + parameter)
|
|
|
|
def response_delete(self, request, obj_display, obj_id):
|
|
"""
|
|
Determine the HttpResponse for the delete_view stage. Clear the user's permission
|
|
lru cache
|
|
"""
|
|
clear_permission_lru_caches(request.user)
|
|
return super().response_delete(request, obj_display, obj_id)
|
|
|
|
def delete_model(self, request, obj):
|
|
operation_token = send_pre_page_operation(
|
|
request=request, operation=operations.DELETE_PAGE, obj=obj, sender=self.model
|
|
)
|
|
|
|
cms_pages = [obj]
|
|
|
|
if obj.is_branch:
|
|
descendant_ids = obj.get_descendants().values_list("id", flat=True)
|
|
cms_pages.extend(self.model.objects.filter(id__in=descendant_ids))
|
|
|
|
# Delete all associated pages contents
|
|
ct_page_content = ContentType.objects.get_for_model(PageContent)
|
|
page_content_objs = PageContent.admin_manager.filter(page__in=cms_pages).values_list("pk", flat=True)
|
|
placeholders = Placeholder.objects.filter(
|
|
content_type=ct_page_content,
|
|
object_id__in=page_content_objs,
|
|
)
|
|
plugins = CMSPlugin.objects.filter(placeholder__in=placeholders)
|
|
QuerySet.delete(plugins)
|
|
placeholders.delete()
|
|
|
|
super().delete_model(request, obj)
|
|
|
|
send_post_page_operation(
|
|
request=request,
|
|
operation=operations.DELETE_PAGE,
|
|
token=operation_token,
|
|
obj=obj,
|
|
sender=self.model,
|
|
)
|
|
|
|
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")
|
|
|
|
site = get_site_from_request(request)
|
|
user = request.user
|
|
can_view_page = page_permissions.user_can_view_page(user, page, page.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, 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_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_view_permission(self, request, obj=None):
|
|
return self.has_change_permission(request, obj)
|
|
|
|
def has_change_permissions_permission(self, request, obj=None):
|
|
if not obj:
|
|
return False
|
|
return page_permissions.user_can_change_page_permissions(request.user, page=obj, site=obj.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
|
|
return page_permissions.user_can_delete_page(request.user, page=obj, site=obj.site)
|
|
|
|
def has_move_page_permission(self, request, obj=None):
|
|
if not obj:
|
|
return False
|
|
return page_permissions.user_can_move_page(user=request.user, page=obj, site=obj.site)
|
|
|
|
@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"
|
|
|
|
"""
|
|
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=page.site)
|
|
|
|
if not form.is_valid():
|
|
return jsonify_request(HttpResponseBadRequest(str(form.errors.get("__all__", _("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 = _("Error! You don't have permissions to move this page. Please reload the page")
|
|
return jsonify_request(HttpResponseForbidden(message))
|
|
|
|
# The whole subtree moves along, so authorize the whole subtree: the
|
|
# destination may grant access that the source did not.
|
|
target_page, position = form.get_tree_options()
|
|
if position in ("first-child", "last-child"):
|
|
parent_page = target_page
|
|
else:
|
|
parent_page = target_page.parent if target_page else None
|
|
|
|
if not page_permissions.user_can_move_descendants(user, page, page.site, parent_page):
|
|
message = _("Error! You don't have permissions to move this page. Please reload the page")
|
|
return jsonify_request(HttpResponseForbidden(message))
|
|
|
|
operation_token = send_pre_page_operation(
|
|
request=request,
|
|
operation=operations.MOVE_PAGE,
|
|
obj=page,
|
|
sender=self.model,
|
|
)
|
|
|
|
form.move_page(user=user)
|
|
|
|
send_post_page_operation(
|
|
request=request,
|
|
operation=operations.MOVE_PAGE,
|
|
token=operation_token,
|
|
obj=page,
|
|
sender=self.model,
|
|
)
|
|
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)
|
|
|
|
if not self.has_view_permission(request, obj=page):
|
|
raise PermissionDenied("No permission for page permissions view")
|
|
|
|
site = get_site_from_request(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 = page_permissions.get_change_perm_tuples(user, site, check_global=False)
|
|
|
|
for permission in _page_permissions.iterator():
|
|
if can_change_global_permissions:
|
|
can_change = True
|
|
else:
|
|
page_path = permission.page.path
|
|
can_change = any(PermissionTuple(perm_tuple).contains(page_path) for perm_tuple 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_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 = get_site_from_request(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"]
|
|
|
|
# User can only copy pages he can see
|
|
can_copy_page = page_permissions.user_can_view_page(user, page, page.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 can_copy_page:
|
|
target_page, position = form.get_tree_options()
|
|
if position in ("first-child", "last-child"):
|
|
parent_page = target_page
|
|
else:
|
|
parent_page = target_page.parent if target_page else None
|
|
can_copy_page = page_permissions.user_can_copy_descendants(
|
|
user, page, site, parent_page, form.cleaned_data["copy_permissions"]
|
|
)
|
|
|
|
if not can_copy_page:
|
|
message = _("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 = _(
|
|
"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(user)
|
|
return HttpResponse(json.dumps({"id": new_page.pk}), content_type="application/json")
|
|
|
|
def edit_title_fields(self, request, page_id, language):
|
|
page = self.get_object(request, object_id=page_id)
|
|
|
|
if page is None:
|
|
raise self._get_404_exception(page_id)
|
|
|
|
if not self.has_change_permission(request, obj=page):
|
|
return HttpResponseForbidden(_("You do not have permission to edit this page"))
|
|
|
|
translation = page.get_admin_content(language)
|
|
|
|
if not translation:
|
|
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 = PageContent._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 = PageContent
|
|
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)
|
|
|
|
|
|
@admin.register(PageContent)
|
|
class PageContentAdmin(PageDeleteMessageMixin, admin.ModelAdmin):
|
|
ordering = ("page__path",)
|
|
search_fields = ("=id", "page__id", "page__urls__slug", "title", "page__reverse_id")
|
|
change_form_template = "admin/cms/page/change_form.html"
|
|
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"
|
|
|
|
class Media:
|
|
css = {"all": (static_with_version("cms/css/cms.admin.css"),)}
|
|
|
|
form = AddPageForm
|
|
add_form = form
|
|
change_form = ChangePageForm
|
|
copy_form = CopyPageForm
|
|
move_form = MovePageForm
|
|
changelist_form = ChangeListForm
|
|
duplicate_form = DuplicatePageForm
|
|
|
|
def log_addition(self, request, object, object_repr):
|
|
# Block the admin log for addition. A signal takes care of this!
|
|
return
|
|
|
|
def log_deletion(self, request, object, object_repr):
|
|
# DJANGO_42
|
|
# Block the admin log for deletion. A signal takes care of this!
|
|
return
|
|
|
|
def log_deletions(self, request, queryset):
|
|
# Block the admin log for deletion. A signal takes care of this!
|
|
return
|
|
|
|
def log_change(self, request, object, message):
|
|
# Block the admin log for change. A signal takes care of this!
|
|
return
|
|
|
|
def get_admin_url(self, action, *args):
|
|
url_name = f"{self.opts.app_label}_{self.opts.model_name}_{action}"
|
|
return admin_reverse(url_name, args=args)
|
|
|
|
def get_preserved_filters(self, request):
|
|
"""
|
|
This override is in place to preserve the "language" get parameter in
|
|
the "Save" page redirect
|
|
"""
|
|
site = get_site_from_request(request)
|
|
preserved_filters_encoded = super().get_preserved_filters(request)
|
|
preserved_filters = QueryDict(preserved_filters_encoded).copy()
|
|
lang = get_site_language_from_request(request, site_id=site.pk)
|
|
|
|
if lang:
|
|
preserved_filters["language"] = lang
|
|
if site and site.pk != get_current_site(request).pk:
|
|
preserved_filters["site"] = site.pk
|
|
return preserved_filters.urlencode()
|
|
|
|
def get_queryset(self, request):
|
|
return super().get_queryset(request).select_related("page")
|
|
|
|
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"^get-tree/$", self.get_tree),
|
|
pat(r"^([0-9]+)/duplicate/$", self.duplicate),
|
|
pat(r"^([0-9]+)/copy-language/$", self.copy_language),
|
|
pat(r"^([0-9]+)/change-navigation/$", self.change_innavigation),
|
|
pat(r"^([0-9]+)/change-template/$", self.change_template),
|
|
]
|
|
return url_patterns + super().get_urls()
|
|
|
|
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_form_class(self, request, obj=None, **kwargs):
|
|
if "change" in request.path_info:
|
|
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._site = get_site_from_request(request)
|
|
form._request = request
|
|
return form
|
|
|
|
def duplicate(self, request, object_id):
|
|
"""
|
|
Leverages the add view logic to duplicate the page.
|
|
"""
|
|
obj = self.get_object(request, object_id=object_id)
|
|
|
|
if obj 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"] = obj.page_id
|
|
return self.add_view(request)
|
|
|
|
def add_view(self, request, form_url="", extra_context=None):
|
|
site = get_site_from_request(request)
|
|
language = get_site_language_from_request(request, site_id=site.pk)
|
|
|
|
if extra_context is None:
|
|
extra_context = {}
|
|
|
|
try:
|
|
page_id = request.GET.get("cms_page") or request.POST.get("cms_page")
|
|
page_id = IntegerField().clean(page_id)
|
|
cms_page = Page.objects.get(pk=page_id)
|
|
except (ValidationError, Page.DoesNotExist):
|
|
cms_page = None
|
|
|
|
if cms_page:
|
|
# Adding content for an existing page in a language it does not have yet
|
|
extra_context["title"] = _("Add Translation")
|
|
elif "duplicate" in request.path_info:
|
|
extra_context["title"] = _("Add Page Copy")
|
|
elif "parent_page" in request.GET:
|
|
extra_context["title"] = _("New sub page")
|
|
else:
|
|
extra_context["title"] = _("New page")
|
|
|
|
if cms_page:
|
|
extra_context["cms_page"] = cms_page
|
|
extra_context["language_tabs"] = get_language_tuple(site.pk)
|
|
extra_context["filled_languages"] = self.get_filled_languages(request, cms_page)
|
|
extra_context["show_language_tabs"] = len(extra_context["language_tabs"])
|
|
extra_context["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 PageContent model.
|
|
"""
|
|
if extra_context is None:
|
|
extra_context = {"basic_info": True}
|
|
|
|
obj = self.get_object(request, object_id=object_id)
|
|
|
|
if obj is None:
|
|
raise self._get_404_exception(object_id)
|
|
|
|
site = obj.page.site
|
|
context = {
|
|
"cms_page": obj.page,
|
|
"CMS_PERMISSION": get_cms_setting("PERMISSION"),
|
|
"can_change": self.has_change_permission(request, obj=obj),
|
|
"language": obj.language,
|
|
"language_tabs": get_language_tuple(site.pk),
|
|
"filled_languages": self.get_filled_languages(request, obj.page),
|
|
}
|
|
# Only offer the language selector for the latest content. Switching the language always
|
|
# navigates to the latest content of the target language, so for an older content object
|
|
# (e.g. an outdated version) switching languages back and forth would silently bring up a
|
|
# different (the latest) content object - confusing UX.
|
|
latest = obj.page.get_admin_content(obj.language)
|
|
is_latest_content = getattr(latest, "pk", None) == obj.pk
|
|
context["show_language_tabs"] = len(context["language_tabs"]) if is_latest_content else 0
|
|
context.update(extra_context or {})
|
|
|
|
if "basic_info" in extra_context:
|
|
_has_advanced_settings_perm = self.has_change_advanced_settings_permission(request, obj=obj)
|
|
context["can_change_advanced_settings"] = _has_advanced_settings_perm
|
|
|
|
return super().change_view(request, object_id, form_url=form_url, extra_context=context)
|
|
|
|
def response_add(self, request, obj):
|
|
redirect = request.POST.get("edit", False)
|
|
if redirect == "1":
|
|
from django.core.cache import cache
|
|
|
|
from cms.cache.permissions import get_cache_key, get_cache_permission_version
|
|
|
|
cache.delete(
|
|
get_cache_key(request.user, obj.page.site, "change_page"),
|
|
version=get_cache_permission_version(),
|
|
)
|
|
|
|
# redirect to the edit view if added from the toolbar
|
|
url = get_object_edit_url(obj) # Redirects to preview if necessary
|
|
return HttpResponse(MODAL_HTML_REDIRECT.format(url=url))
|
|
return super().response_add(request, obj)
|
|
|
|
def get_filled_languages(self, request, page):
|
|
site_id = page.site_id
|
|
filled_languages = page.get_languages()
|
|
allowed_languages = [lang[0] for lang in get_language_tuple(site_id)]
|
|
return [lang for lang in filled_languages if lang in allowed_languages]
|
|
|
|
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):
|
|
if parent_id := request.GET.get("parent_page"):
|
|
try:
|
|
parent_id = IntegerField().clean(parent_id)
|
|
parent_item = Page.objects.get(id=parent_id)
|
|
except (ValidationError, Page.DoesNotExist):
|
|
return False
|
|
else:
|
|
parent_item = None
|
|
|
|
if parent_item:
|
|
has_perm = page_permissions.user_can_add_subpage(
|
|
request.user,
|
|
target=parent_item,
|
|
site=parent_item.site,
|
|
)
|
|
else:
|
|
site = get_site_from_request(request)
|
|
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.
|
|
"""
|
|
|
|
if obj:
|
|
return page_permissions.user_can_change_page(request.user, page=obj.page, site=obj.page.site)
|
|
return page_permissions.user_can_change_at_least_one_page(
|
|
user=request.user,
|
|
site=get_site_from_request(request),
|
|
)
|
|
|
|
def has_view_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.
|
|
"""
|
|
# Identical to has_change_permission, but will remain untouched by any subclassing
|
|
# as done, e.g., by djangocms-versioning
|
|
|
|
if obj:
|
|
return page_permissions.user_can_change_page(request.user, page=obj.page, site=obj.page.site)
|
|
return page_permissions.user_can_change_at_least_one_page(
|
|
user=request.user,
|
|
site=get_site_from_request(request),
|
|
use_cache=False,
|
|
)
|
|
|
|
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
|
|
return page_permissions.user_can_delete_page(request.user, page=obj.page, site=obj.page.site)
|
|
|
|
def has_change_advanced_settings_permission(self, request, obj=None):
|
|
if not obj:
|
|
return False
|
|
return page_permissions.user_can_change_page_advanced_settings(request.user, page=obj.page, site=obj.page.site)
|
|
|
|
def has_delete_translation_permission(self, request, language, obj=None):
|
|
if not obj:
|
|
return False
|
|
|
|
has_perm = page_permissions.user_can_delete_page_translation(
|
|
user=request.user,
|
|
page=obj,
|
|
language=language,
|
|
site=obj.site,
|
|
)
|
|
return has_perm
|
|
|
|
def get_sites_for_user(self, user):
|
|
from django.contrib.sites.models import Site
|
|
|
|
if hasattr(user, "_cms_user_sites"):
|
|
return user._cms_user_sites
|
|
|
|
sites = Site.objects.order_by("name")
|
|
if not get_cms_setting("PERMISSION") or user.is_superuser:
|
|
user._cms_user_sites = sites
|
|
return sites
|
|
|
|
_has_perm = page_permissions.user_can_change_at_least_one_page
|
|
user_sites = [site for site in sites if _has_perm(user, site)]
|
|
user._cms_user_sites = user_sites
|
|
return user_sites
|
|
|
|
def user_can_access_site(self, request):
|
|
"""
|
|
.. deprecated:: 5.2
|
|
Unused. Site isolation is enforced by ``has_change_permission``,
|
|
which checks ``user_can_change_at_least_one_page`` against the site
|
|
of the request.
|
|
"""
|
|
import warnings
|
|
|
|
warnings.warn(
|
|
"PageContentAdmin.user_can_access_site() is deprecated and unused. "
|
|
"Use has_change_permission(request) instead, which already scopes the check to the "
|
|
"site of the request.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
site = get_site_from_request(request)
|
|
user_sites = self.get_sites_for_user(request.user)
|
|
return site in user_sites
|
|
|
|
def raise_site_permission_denied(self):
|
|
"""
|
|
.. deprecated:: 5.2
|
|
Unused. Raise ``django.core.exceptions.PermissionDenied`` directly.
|
|
"""
|
|
import warnings
|
|
|
|
warnings.warn(
|
|
"PageContentAdmin.raise_site_permission_denied() is deprecated and unused. "
|
|
"Raise django.core.exceptions.PermissionDenied directly instead.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
raise PermissionDenied(_("You do not have permission to access this site. Please contact your administrator."))
|
|
|
|
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
|
|
|
|
site = get_site_from_request(request)
|
|
language = get_site_language_from_request(request, site_id=site.pk)
|
|
query = request.GET.get("q", "")
|
|
page_contents = self.get_queryset(request)
|
|
page_contents = self.get_search_results(request, page_contents, query)[0]
|
|
changelist_form = self.changelist_form(request.GET)
|
|
|
|
try:
|
|
changelist_form.full_clean()
|
|
page_contents = changelist_form.run_filters(page_contents)
|
|
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")
|
|
|
|
pages = Page.objects.on_site(site).filter(pagecontent_set__in=page_contents).distinct().order_by("path")
|
|
pages = pages.prefetch_related(
|
|
Prefetch(
|
|
"pagecontent_set",
|
|
to_attr="filtered_translations",
|
|
queryset=page_contents,
|
|
),
|
|
"urls", # rendering a tree row resolves the page's URLs
|
|
)
|
|
|
|
if changelist_form.is_filtered():
|
|
# 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_tuple(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_content = self.get_object(request, object_id=object_id)
|
|
|
|
if not self.has_change_permission(request, obj=page_content):
|
|
raise PermissionDenied("No permissions to change the template")
|
|
|
|
if page_content is None:
|
|
raise self._get_404_exception(object_id)
|
|
|
|
to_template = request.POST.get("template", None)
|
|
|
|
if get_cms_setting("TEMPLATES"):
|
|
if to_template not in dict(get_cms_setting("TEMPLATES")):
|
|
return HttpResponseBadRequest(_("Template not valid"))
|
|
else:
|
|
if to_template not in (placeholder_set[0] for placeholder_set in get_cms_setting("PLACEHOLDERS")):
|
|
return HttpResponseBadRequest(_("Placeholder selection not valid"))
|
|
|
|
page_content.template = to_template
|
|
page_content.save()
|
|
|
|
return HttpResponse(_("The template was successfully changed"))
|
|
|
|
@require_POST
|
|
@transaction.atomic
|
|
def copy_language(self, request, object_id):
|
|
target_language = request.POST.get("target_language")
|
|
source_page_content = self.get_object(request, object_id=object_id)
|
|
|
|
if not self.has_change_permission(request, obj=source_page_content):
|
|
raise PermissionDenied
|
|
|
|
if source_page_content is None:
|
|
raise self._get_404_exception(object_id)
|
|
|
|
page = source_page_content.page
|
|
|
|
if not target_language or target_language not in get_language_list(site_id=page.site_id):
|
|
return HttpResponseBadRequest(_("Language must be set to a supported language!"))
|
|
|
|
target_page_content = page.get_content_obj(target_language, fallback=False)
|
|
|
|
source_placeholders = list(source_page_content.get_placeholders())
|
|
target_placeholders = {
|
|
placeholder.slot: placeholder
|
|
for placeholder in target_page_content.get_placeholders()
|
|
}
|
|
source_plugins = downcast_plugins(
|
|
CMSPlugin.objects.filter(
|
|
placeholder_id__in=[placeholder.pk for placeholder in source_placeholders],
|
|
language=source_page_content.language,
|
|
).order_by("position")
|
|
)
|
|
plugins_by_placeholder = defaultdict(list)
|
|
for plugin in source_plugins:
|
|
plugins_by_placeholder[plugin.placeholder_id].append(plugin)
|
|
|
|
for placeholder in source_placeholders:
|
|
target = target_placeholders.get(placeholder.slot)
|
|
if target is None:
|
|
messages.warning(request, _("Placeholder '%s' does not exist in target language") % placeholder.slot)
|
|
continue
|
|
plugins = plugins_by_placeholder[placeholder.pk]
|
|
|
|
if not target.has_add_plugins_permission(request.user, plugins):
|
|
return HttpResponseForbidden(_("You do not have permission to copy these plugins."))
|
|
copy_plugins_to_placeholder(
|
|
plugins,
|
|
target,
|
|
language=target_language,
|
|
plugins_are_downcast=True,
|
|
)
|
|
return HttpResponse("ok")
|
|
|
|
def delete_view(self, request, object_id, extra_context=None):
|
|
page_content = self.get_object(request, object_id=object_id)
|
|
|
|
if page_content is None:
|
|
raise self._get_404_exception(object_id)
|
|
|
|
page = page_content.page
|
|
|
|
if not self.has_delete_translation_permission(request, page_content.language, page):
|
|
return HttpResponseForbidden(_("You do not have permission to delete this page"))
|
|
|
|
if not len(list(page.get_languages())) > 1:
|
|
return HttpResponseBadRequest("There only exists one translation for this page")
|
|
|
|
return super().delete_view(request, object_id, extra_context)
|
|
|
|
def delete_model(self, request, obj):
|
|
ct_page_content = ContentType.objects.get_for_model(PageContent)
|
|
page_contents = PageContent.admin_manager.filter(page=obj.page, language=obj.language)
|
|
placeholders = Placeholder.objects.filter(
|
|
content_type=ct_page_content,
|
|
object_id__in=page_contents.values("pk"),
|
|
)
|
|
saved_plugins = CMSPlugin.objects.filter(placeholder__in=placeholders)
|
|
# A translation that has never been published may have no PageUrl
|
|
page_urls = obj.page.urls.filter(language=obj.language)
|
|
|
|
operation_token = send_pre_page_operation(
|
|
request=request,
|
|
operation=operations.DELETE_PAGE_TRANSLATION,
|
|
obj=obj.page,
|
|
translation=obj,
|
|
sender=self.model,
|
|
)
|
|
|
|
message = _("Title and plugins with language %(language)s was deleted") % {
|
|
"language": force_str(get_language_object(obj.language, site_id=obj.page.site_id)["name"])
|
|
}
|
|
messages.success(request, message)
|
|
if obj.page.admin_content_cache and obj.language in obj.page.admin_content_cache:
|
|
del obj.page.admin_content_cache[obj.language]
|
|
if obj.language in obj.page.page_content_cache:
|
|
del obj.page.page_content_cache[obj.language]
|
|
|
|
page_urls.delete()
|
|
page_contents.delete()
|
|
saved_plugins.delete()
|
|
|
|
send_post_page_operation(
|
|
request=request,
|
|
operation=operations.DELETE_PAGE_TRANSLATION,
|
|
token=operation_token,
|
|
obj=obj.page,
|
|
translation=obj,
|
|
sender=self.model,
|
|
)
|
|
|
|
@require_POST
|
|
def change_innavigation(self, request, object_id):
|
|
"""
|
|
Switch the in_navigation of a page
|
|
"""
|
|
page_content = self.get_object(request, object_id=object_id)
|
|
|
|
if not self.has_change_permission(request, obj=page_content):
|
|
if self.has_change_permission(request):
|
|
# General (permission) problem
|
|
message = "You do not have permission to change a page's navigation status"
|
|
else:
|
|
# Only this page? Can be permissions or versioning, or ...
|
|
message = "You cannot change this page's navigation status"
|
|
return HttpResponseForbidden(_(message))
|
|
|
|
if page_content is None:
|
|
raise self._get_404_exception(object_id)
|
|
|
|
page_content.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
|
|
"""
|
|
if not self.has_change_permission(request, obj=None):
|
|
raise PermissionDenied
|
|
site = get_site_from_request(request)
|
|
pages = Page.objects.on_site(site).order_by("path")
|
|
node_id = re.sub(r"[^\d]", "", request.GET.get("nodeId", "")) or None
|
|
open_page_ids = [int(id) for id in request.GET.getlist("openNodes[]") if id.isdigit()]
|
|
if node_id:
|
|
page = get_object_or_404(pages, id=node_id)
|
|
pages = page.get_descendant_pages().filter(Q(id__in=open_page_ids) | Q(parent__in=open_page_ids))
|
|
else:
|
|
page = None
|
|
pages = pages.filter(
|
|
# get all root pages or children which were previously open or children of the open descendants
|
|
Q(depth=1) | Q(depth=2, id__in=open_page_ids) | Q(parent__in=open_page_ids)
|
|
)
|
|
pages = pages.prefetch_related(
|
|
Prefetch(
|
|
"pagecontent_set",
|
|
to_attr="filtered_translations",
|
|
queryset=PageContent.admin_manager.get_queryset().latest_content(),
|
|
),
|
|
"urls", # rendering a tree row resolves the page's URLs
|
|
)
|
|
rows = self.get_tree_rows(
|
|
request,
|
|
pages=pages,
|
|
language=get_site_language_from_request(request, site_id=site.pk),
|
|
depth=(page.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
|
|
"""
|
|
if not self.has_change_permission(request, obj=None):
|
|
raise PermissionDenied
|
|
site = get_site_from_request(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
|
|
user_can_change = page_permissions.user_can_change_page
|
|
user_can_change_advanced = page_permissions.user_can_change_page_advanced_settings
|
|
user_can_change_permissions = page_permissions.user_can_change_page_permissions
|
|
|
|
def render_page_row(page):
|
|
page.admin_content_cache = AdminCacheDict(
|
|
(trans.language, trans) for trans in page.filtered_translations
|
|
)
|
|
has_move_page_permission = page_permissions.user_can_move_page(request.user, page, site=site)
|
|
|
|
if permissions_on and not has_move_page_permission:
|
|
# TODO: check if this is really needed
|
|
metadata = '{"valid_children": False, "draggable": False}'
|
|
else:
|
|
metadata = ""
|
|
|
|
page_content = page.get_admin_content(language)
|
|
context = {
|
|
"admin": self,
|
|
"opts": self.opts,
|
|
"site": site,
|
|
"page": page,
|
|
"page_content": page_content,
|
|
"ancestors": [page for page in page.get_cached_ancestors()],
|
|
"descendants": [page for page in page.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(request.user, target=page),
|
|
"has_change_permission": user_can_change(request.user, page, site),
|
|
"has_change_advanced_settings_permission": (
|
|
user_can_change_advanced(request.user, page, site)
|
|
or user_can_change_permissions(request.user, page, site)
|
|
),
|
|
"has_move_page_permission": has_move_page_permission,
|
|
"can_change": page_content.is_editable(request),
|
|
}
|
|
context["is_concrete"] = context["page_content"].language == language
|
|
return template.render(context)
|
|
|
|
if follow_descendants:
|
|
root_pages = (page for page in pages if page.depth == depth)
|
|
else:
|
|
# When the tree is filtered, it's displayed as a flat structure
|
|
root_pages = pages
|
|
|
|
if depth == 1:
|
|
for page in root_pages:
|
|
page._set_hierarchy(list(pages))
|
|
yield render_page_row(page)
|
|
else:
|
|
for page in root_pages:
|
|
yield render_page_row(page)
|
|
|
|
# Indicators in the page tree
|
|
@property
|
|
def indicator_descriptions(self):
|
|
return {
|
|
"public": _("Public content"),
|
|
"empty": _("Empty"),
|
|
}
|
|
|
|
@classmethod
|
|
def get_indicator_menu(cls, request, page_content):
|
|
menu_template = "admin/cms/page/tree/indicator_menu.html"
|
|
if not page_content:
|
|
return menu_template, [
|
|
(
|
|
_("Create Content"), # Entry
|
|
"cms-icon-edit-new", # Optional icon
|
|
admin_reverse("cms_pagecontent_add")
|
|
+ f"?cms_page={page_content.page.pk}&language={page_content.language}", # url
|
|
None, # Optional add classes for <a>
|
|
),
|
|
]
|
|
return "", []
|