forked from ungleich-public/dynamicweb
1379 lines
53 KiB
Python
1379 lines
53 KiB
Python
import warnings
|
|
from logging import getLogger
|
|
from os.path import join
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.sites.models import Site
|
|
from django.db import IntegrityError, connection, models, router, transaction
|
|
from django.db.models import Prefetch
|
|
from django.db.models.base import ModelState
|
|
from django.db.models.constraints import UniqueConstraint
|
|
from django.db.models.functions import Concat
|
|
from django.forms import model_to_dict
|
|
from django.urls import NoReverseMatch, reverse
|
|
from django.utils.encoding import force_str
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import get_language, gettext_lazy as _, override as force_language
|
|
|
|
from cms import constants
|
|
from cms.models.managers import PageManager, PageUrlManager
|
|
from cms.utils import i18n
|
|
from cms.utils.compat.warnings import RemovedInDjangoCMS60Warning
|
|
from cms.utils.conf import get_cms_setting
|
|
from cms.utils.i18n import get_current_language
|
|
from cms.utils.mptree import get_tree_base
|
|
from cms.utils.page import get_clean_username
|
|
from menus.menu_pool import menu_pool
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
|
|
def _lock_tree_roots(*pages):
|
|
# Serializes concurrent URL path updates by taking a row-level lock on the
|
|
# tree root(s) of the given pages, which prevents deadlocks on cms_pageurl.
|
|
# Locks are acquired in pk order to avoid deadlocks between the locks
|
|
# themselves when two roots are involved.
|
|
assert connection.in_atomic_block, "_lock_tree_roots requires an active transaction"
|
|
pks = sorted({page.get_root().pk for page in pages})
|
|
list(Page.objects.filter(pk__in=pks).order_by("pk").select_for_update())
|
|
|
|
|
|
class AdminCacheDict(dict):
|
|
"""Dictionary that disallows setting individual items to prevent accidental cache corruption."""
|
|
|
|
def __setitem__(self, key, value):
|
|
raise ValueError("Do not set individual items in the admin cache dict. Use the clear_cache method instead.")
|
|
|
|
|
|
MP_Node = get_tree_base()
|
|
|
|
class Page(MP_Node):
|
|
"""
|
|
A ``Page`` is the basic unit of site structure in django CMS. The CMS uses a hierarchical page model: each page
|
|
stands in relation to other pages as parent, child or sibling. This hierarchy is managed by the `django-treebeard
|
|
<http://django-treebeard.readthedocs.io/en/latest/>`_ library.
|
|
|
|
A ``Page`` is an abstract entity. It does not have any content associated with it, nor does is provide any slugs to
|
|
build a URL. This is part of the model :class:`~cms.models.contentmodels.PageContent` and
|
|
:class:`~cms.models.pagemodel.PageUrl` respectively.
|
|
"""
|
|
|
|
parent = models.ForeignKey(
|
|
"self",
|
|
on_delete=models.CASCADE,
|
|
blank=True,
|
|
null=True,
|
|
related_name="children",
|
|
db_index=True,
|
|
)
|
|
site = models.ForeignKey(
|
|
Site,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("site"),
|
|
related_name="djangocms_pages",
|
|
db_index=True,
|
|
)
|
|
created_by = models.CharField(
|
|
_("created by"),
|
|
max_length=constants.PAGE_USERNAME_MAX_LENGTH,
|
|
editable=False,
|
|
)
|
|
changed_by = models.CharField(
|
|
_("changed by"),
|
|
max_length=constants.PAGE_USERNAME_MAX_LENGTH,
|
|
editable=False,
|
|
)
|
|
creation_date = models.DateTimeField(auto_now_add=True)
|
|
changed_date = models.DateTimeField(auto_now=True)
|
|
|
|
#
|
|
# Please use toggle_in_navigation() instead of affecting this property
|
|
# directly so that the cms page cache can be invalidated as appropriate.
|
|
#
|
|
reverse_id = models.CharField(
|
|
_("id"),
|
|
max_length=40,
|
|
db_index=True,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_("A unique identifier that is used with the page_url templatetag for linking to this page"),
|
|
)
|
|
navigation_extenders = models.CharField(
|
|
_("attached menu"),
|
|
max_length=80,
|
|
db_index=True,
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
login_required = models.BooleanField(
|
|
_("login required"),
|
|
default=False,
|
|
)
|
|
is_home = models.BooleanField(
|
|
editable=False,
|
|
db_index=True,
|
|
default=False,
|
|
)
|
|
application_urls = models.CharField(
|
|
_("application"),
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
db_index=True,
|
|
)
|
|
application_namespace = models.CharField(
|
|
_("application instance name"),
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
is_page_type = models.BooleanField(
|
|
default=False,
|
|
help_text=_("Mark this page as a page type"),
|
|
)
|
|
|
|
# Managers
|
|
objects = PageManager()
|
|
|
|
class Meta:
|
|
default_permissions = ["add", "change", "delete"]
|
|
permissions = [
|
|
("view_page", "Can view page"),
|
|
("publish_page", "Can publish page"),
|
|
("edit_static_placeholder", "Can edit static placeholders"),
|
|
]
|
|
verbose_name = _("page")
|
|
verbose_name_plural = _("pages")
|
|
app_label = "cms"
|
|
ordering = ["path"]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.urls_cache = {}
|
|
#: Internal cache for page urls
|
|
self.page_content_cache = {}
|
|
#: Internal cache for page content objects visible publicly
|
|
self.admin_content_cache = None
|
|
#: Internal cache for page content objects visible in the admin (i.e. to staff users.)
|
|
#: Might be larger than the page_content_cache
|
|
|
|
def __str__(self):
|
|
page_content = self.get_admin_content(get_language(), fallback=True)
|
|
if page_content:
|
|
title = page_content.menu_title or page_content.title
|
|
else:
|
|
title = _("No available title")
|
|
return force_str(title)
|
|
|
|
def __repr__(self):
|
|
display = f"<{self.__module__}.{self.__class__.__name__} id={self.pk} object at {hex(id(self))}>"
|
|
return display
|
|
|
|
@property
|
|
def node(self):
|
|
warnings.warn(
|
|
"The `node` has been removed from Page objects. Access the TreeNode attributes directly.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
return self
|
|
|
|
@property
|
|
def is_branch(self):
|
|
return bool(self.numchild)
|
|
|
|
def get_ancestor_paths(self):
|
|
paths = frozenset(self.path[0:pos] for pos in range(0, len(self.path), self.steplen)[1:])
|
|
return paths
|
|
|
|
def add_child(self, **kwargs):
|
|
if len(kwargs) == 1 and "instance" in kwargs:
|
|
kwargs["instance"].parent = self
|
|
else:
|
|
kwargs["parent"] = self
|
|
return super().add_child(**kwargs)
|
|
|
|
def add_sibling(self, pos=None, *args, **kwargs):
|
|
if len(kwargs) == 1 and "instance" in kwargs:
|
|
kwargs["instance"].parent_id = self.parent_id
|
|
else:
|
|
kwargs["parent_id"] = self.parent_id
|
|
return super().add_sibling(*args, **kwargs)
|
|
|
|
def get_cached_ancestors(self):
|
|
if self._has_cached_hierarchy():
|
|
return self._ancestors
|
|
return []
|
|
|
|
def get_cached_descendants(self):
|
|
if self._has_cached_hierarchy():
|
|
return self._descendants
|
|
return []
|
|
|
|
def _clear_internal_cache(self):
|
|
self.urls_cache = {}
|
|
self.page_content_cache = {}
|
|
self.admin_content_cache = None
|
|
|
|
if hasattr(self, "_prefetched_objects_cache"):
|
|
del self._prefetched_objects_cache
|
|
|
|
@property
|
|
def parent_page(self):
|
|
warnings.warn(
|
|
"Attribute `parent_page` is deprecated. Instead use the attribute `parent`.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
return self.parent
|
|
|
|
def has_valid_url(self, language):
|
|
return self.urls.filter(language=language, path__isnull=False).exists()
|
|
|
|
def set_as_homepage(self, user=None):
|
|
"""
|
|
Sets the given page as the homepage.
|
|
Updates the url paths for all affected pages.
|
|
Returns the old home page (if any).
|
|
"""
|
|
if user:
|
|
changed_by = get_clean_username(user)
|
|
else:
|
|
changed_by = constants.SCRIPT_USERNAME
|
|
|
|
changed_date = now()
|
|
|
|
try:
|
|
old_home = self.__class__.objects.get(
|
|
is_home=True,
|
|
site=self.site_id,
|
|
)
|
|
except self.__class__.DoesNotExist:
|
|
_lock_tree_roots(self)
|
|
old_home_tree = []
|
|
else:
|
|
_lock_tree_roots(self, old_home)
|
|
|
|
old_home.update(
|
|
is_home=False,
|
|
changed_by=changed_by,
|
|
changed_date=changed_date,
|
|
)
|
|
old_home_tree = old_home._set_title_root_path()
|
|
|
|
self.update(
|
|
is_home=True,
|
|
changed_by=changed_by,
|
|
changed_date=changed_date,
|
|
)
|
|
new_home_tree = self._remove_title_root_path()
|
|
return (new_home_tree, old_home_tree)
|
|
|
|
def _has_cached_hierarchy(self):
|
|
return hasattr(self, "_descendants") and hasattr(self, "_ancestors")
|
|
|
|
def _set_hierarchy(self, pages, ancestors=None):
|
|
if self.is_branch:
|
|
self._descendants = [page for page in pages if page.path.startswith(self.path) and page.depth > self.depth]
|
|
else:
|
|
self._descendants = []
|
|
|
|
if self.is_root():
|
|
self._ancestors = []
|
|
else:
|
|
self._ancestors = ancestors
|
|
|
|
for child in self._descendants:
|
|
if child.depth == self.depth + 1:
|
|
child._set_hierarchy(self._descendants, ancestors=([self] + self._ancestors))
|
|
|
|
def _get_path_sql_value(self, base_path=""):
|
|
if base_path:
|
|
new_path = Concat(
|
|
models.Value(base_path),
|
|
models.Value("/"),
|
|
models.F("slug"),
|
|
output_field=models.CharField(),
|
|
)
|
|
elif base_path is None:
|
|
new_path = None
|
|
else:
|
|
# the homepage
|
|
new_path = models.F("slug")
|
|
return new_path
|
|
|
|
def _update_url_path(self, language):
|
|
base_path = self.parent.get_path(language) if self.parent else ""
|
|
new_path = self._get_path_sql_value(base_path)
|
|
|
|
(
|
|
PageUrl.objects.filter(language=language, page=self).exclude(managed=False).update(path=new_path)
|
|
) # TODO: Update or create?
|
|
|
|
def _update_url_path_recursive(self, language):
|
|
"""Rebuild the managed URL paths of all descendant pages for
|
|
``language``, level by level: each page's path derives from its
|
|
parent's effective path. Unmanaged (overwritten) URLs keep their fixed
|
|
path but still serve as the base for their own descendants.
|
|
|
|
Costs one SELECT and at most one bulk UPDATE per tree level instead of
|
|
two queries per descendant page.
|
|
"""
|
|
if self.is_leaf() or language not in self.get_languages():
|
|
return
|
|
|
|
base_paths = {self.pk: self.get_path(language)}
|
|
|
|
while base_paths:
|
|
urls = PageUrl.objects.filter(page__parent_id__in=base_paths, language=language).values_list(
|
|
"pk", "page_id", "page__parent_id", "slug", "managed", "path"
|
|
)
|
|
changed = []
|
|
next_base_paths = {}
|
|
for pk, page_id, parent_id, slug, managed, path in urls:
|
|
if managed:
|
|
base = base_paths[parent_id]
|
|
if base is None:
|
|
# an unreachable parent makes the page unreachable too
|
|
new_path = None
|
|
else:
|
|
# an empty base means the parent is the home page
|
|
new_path = f"{base}/{slug}" if base else slug
|
|
if new_path != path:
|
|
changed.append(PageUrl(pk=pk, path=new_path))
|
|
next_base_paths[page_id] = new_path
|
|
else:
|
|
next_base_paths[page_id] = path
|
|
if changed:
|
|
PageUrl.objects.bulk_update(changed, ["path"])
|
|
base_paths = next_base_paths
|
|
|
|
def _set_title_root_path(self):
|
|
page_tree = self.__class__.get_tree(self)
|
|
page_urls = PageUrl.objects.filter(page__in=page_tree, managed=True, path__isnull=False)
|
|
|
|
for language, slug in self.urls.values_list("language", "slug"):
|
|
# Update the translations for all descendants of this page
|
|
# to include this page's slug as its path prefix
|
|
(page_urls.filter(language=language).update(path=Concat(models.Value(slug), models.Value("/"), "path")))
|
|
self.update_urls(language, path=slug)
|
|
return page_tree
|
|
|
|
def _remove_title_root_path(self):
|
|
page_tree = self.__class__.get_tree(self)
|
|
page_urls = PageUrl.objects.filter(page__in=page_tree, managed=True, path__isnull=False)
|
|
|
|
for language, slug in self.urls.values_list("language", "slug"):
|
|
# Use 2 because of 1 indexing plus the fact we need to trim
|
|
# the "/" character.
|
|
trim_count = len(slug) + 2
|
|
sql_func = models.Func(
|
|
models.F("path"),
|
|
models.Value(trim_count),
|
|
function="substr",
|
|
)
|
|
(page_urls.filter(language=language, path__startswith=slug).update(path=sql_func))
|
|
return page_tree
|
|
|
|
def is_potential_home(self):
|
|
"""
|
|
Encapsulates logic for determining if this page is eligible to be set
|
|
as `is_home`. This is a public method so that it can be accessed in the
|
|
admin for determining whether to enable the "Set as home" menu item.
|
|
:return: Boolean
|
|
"""
|
|
# Only root nodes are eligible for homepage
|
|
return not self.is_home and bool(self.is_root())
|
|
|
|
def get_absolute_url(self, language=None, fallback=True):
|
|
"""Return the absolute URL of the page for the given language, or
|
|
``None`` if the page has no reachable path (e.g. it or an ancestor
|
|
is unpublished)."""
|
|
if not language:
|
|
language = get_current_language()
|
|
|
|
with force_language(language):
|
|
try:
|
|
if self.is_home:
|
|
return reverse("pages-root")
|
|
path = self.get_path(language, fallback)
|
|
return reverse("pages-details-by-slug", kwargs={"slug": path}) if path else None
|
|
except NoReverseMatch:
|
|
return None
|
|
|
|
def set_tree_node(self, site, target=None, position="first-child"):
|
|
warnings.warn(
|
|
"Method `set_tree_node` is deprecated. Use `add_to_tree` instead.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
self.site = site
|
|
self.parent = target
|
|
return self.add_to_tree(position=position)
|
|
|
|
def add_to_tree(self, position="first-child"):
|
|
assert position in ("last-child", "first-child", "left", "right")
|
|
|
|
if self.parent is None:
|
|
Page.add_root(instance=self)
|
|
elif position == "first-child" and self.parent.is_branch:
|
|
self.parent.get_first_child().add_sibling(pos="left", instance=self)
|
|
elif position in ("last-child", "first-child"):
|
|
self.parent.add_child(instance=self)
|
|
else:
|
|
self.parent.add_sibling(pos=position, instance=self)
|
|
|
|
def move_page(self, target_page, position="first-child", user=None):
|
|
"""
|
|
Called from admin interface when page is moved. Should be used on
|
|
all the places which are changing page position. Used like an interface
|
|
to django-treebeard.
|
|
"""
|
|
assert isinstance(target_page, Page), f"{target_page} is not an instance of Page."
|
|
inherited_template = self.template == constants.TEMPLATE_INHERITANCE_MAGIC
|
|
|
|
# Snapshot before the move: the ancestors protecting this subtree stop
|
|
# doing so as soon as its path changes.
|
|
inherited_restrictions = self.get_view_restrictions(inherited_only=True)
|
|
|
|
if inherited_template and target_page.is_root() and position in ("left", "right"):
|
|
# The page is being moved to a root position.
|
|
# Explicitly set the inherited template on the titles
|
|
# to keep all plugins / placeholders.
|
|
template = self.get_template()
|
|
self.update_translations(template=template)
|
|
|
|
# Don't use a cached node. Always get a fresh one.
|
|
self._clear_internal_cache()
|
|
|
|
# Runs the SQL updates on the treebeard fields
|
|
self.move(target_page, position)
|
|
|
|
if position in ("first-child", "last-child"):
|
|
self.parent = target_page
|
|
else:
|
|
# moving relative to sibling
|
|
# or to the root of the tree
|
|
self.parent = target_page.parent
|
|
# Runs the SQL updates on the parent field
|
|
self.update(parent=self.parent)
|
|
self.refresh_from_db(fields=("path", "depth"))
|
|
|
|
# The subtree left the ancestors it inherited its view restrictions
|
|
# from, so they have to be materialized on its new root.
|
|
self.apply_view_restrictions(inherited_restrictions, user=user)
|
|
|
|
# Update the urls for the page being moved and its descendants.
|
|
_lock_tree_roots(self)
|
|
|
|
languages = self.urls.values_list("language", flat=True)
|
|
|
|
for language in languages:
|
|
if not self.is_home:
|
|
self._update_url_path(language)
|
|
self._update_url_path_recursive(language)
|
|
self.clear_cache(menu=True)
|
|
return self
|
|
|
|
def _clear_placeholders(self, language):
|
|
from cms.models import CMSPlugin
|
|
|
|
placeholders = self.get_placeholders(language)
|
|
placeholder_ids = (placeholder.pk for placeholder in placeholders)
|
|
plugins = CMSPlugin.objects.filter(placeholder__in=placeholder_ids, language=language)
|
|
models.query.QuerySet.delete(plugins)
|
|
return placeholders
|
|
|
|
def get_view_restrictions(self, inherited_only=False):
|
|
"""Snapshot the ``can_view`` grants that currently protect this page.
|
|
|
|
Each entry is a ``(user_id, group_id, grant_on)`` tuple whose scope is
|
|
expressed relative to *this* page: a grant inherited from an ancestor
|
|
becomes ``ACCESS_PAGE_AND_DESCENDANTS`` when it also covers what is
|
|
below this page, and ``ACCESS_PAGE`` when it stops here. The page's own
|
|
rows keep their own scope.
|
|
"""
|
|
from cms.models import ACCESS_PAGE, ACCESS_PAGE_AND_DESCENDANTS, PagePermission
|
|
from cms.models.permissionmodels import MASK_DESCENDANTS
|
|
|
|
if not get_cms_setting("PERMISSION"):
|
|
return []
|
|
|
|
restrictions = [
|
|
(
|
|
permission.user_id,
|
|
permission.group_id,
|
|
ACCESS_PAGE_AND_DESCENDANTS if permission.grant_on & MASK_DESCENDANTS else ACCESS_PAGE,
|
|
)
|
|
for permission in PagePermission.objects.for_page(self).filter(can_view=True)
|
|
if permission.page_id != self.pk
|
|
]
|
|
|
|
if not inherited_only:
|
|
restrictions += [
|
|
(permission.user_id, permission.group_id, permission.grant_on)
|
|
for permission in self.pagepermission_set.filter(can_view=True)
|
|
]
|
|
return restrictions
|
|
|
|
def apply_view_restrictions(self, restrictions, user=None):
|
|
"""Recreate ``restrictions`` (from ``get_view_restrictions``) on this page.
|
|
|
|
Used when a page leaves the subtree its view restrictions came from: the
|
|
ancestors that protected it no longer do, so their grants have to be
|
|
materialized here. Only ``can_view`` is carried over -- the editing
|
|
privileges attached to the original rows are not privileges of the new
|
|
location. Grants the page already inherits at its new position with at
|
|
least the same scope are skipped, so repeated moves do not pile up
|
|
redundant rows.
|
|
"""
|
|
from cms.cache.permissions import clear_permission_cache
|
|
from cms.models import PagePermission
|
|
from cms.models.permissionmodels import MASK_DESCENDANTS
|
|
from cms.utils.permissions import clear_permission_lru_caches
|
|
|
|
if not restrictions or not get_cms_setting("PERMISSION"):
|
|
return []
|
|
|
|
def scope(grant_on):
|
|
return 2 if grant_on & MASK_DESCENDANTS else 1
|
|
|
|
covered = {}
|
|
for permission in PagePermission.objects.for_page(self).filter(can_view=True):
|
|
audience = (permission.user_id, permission.group_id)
|
|
covered[audience] = max(covered.get(audience, 0), scope(permission.grant_on))
|
|
|
|
new_permissions = [
|
|
PagePermission(
|
|
page=self,
|
|
user_id=user_id,
|
|
group_id=group_id,
|
|
grant_on=grant_on,
|
|
**{flag: flag == "can_view" for flag in PagePermission.get_all_permissions()},
|
|
)
|
|
for user_id, group_id, grant_on in restrictions
|
|
if scope(grant_on) > covered.get((user_id, group_id), 0)
|
|
]
|
|
|
|
if new_permissions:
|
|
PagePermission.objects.bulk_create(new_permissions)
|
|
# bulk_create does not emit the permission signals.
|
|
clear_permission_cache()
|
|
if user is not None:
|
|
clear_permission_lru_caches(user)
|
|
transaction.on_commit(clear_permission_cache)
|
|
return new_permissions
|
|
|
|
def copy(
|
|
self,
|
|
site,
|
|
parent_page=None,
|
|
parent_node=None,
|
|
language=None,
|
|
translations=True,
|
|
permissions=False,
|
|
extensions=True,
|
|
user=None,
|
|
):
|
|
from cms.models import PageContent
|
|
from cms.utils.page import get_available_slug
|
|
|
|
assert parent_page is None or isinstance(parent_page, Page), f"{parent_page} is not an instance of Page."
|
|
assert isinstance(user, get_user_model()), f"{user} is not an instance of User."
|
|
|
|
if parent_node is not None:
|
|
warnings.warn(
|
|
"Argument `parent_node` is deprecated. Use `parent_page` instead.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
parent_page = parent_page or parent_node
|
|
|
|
if parent_page:
|
|
new_page = parent_page.add_child(site=site, login_required=self.login_required)
|
|
else:
|
|
new_page = Page.add_root(site=site, login_required=self.login_required)
|
|
|
|
new_page._state = ModelState()
|
|
new_page._clear_internal_cache()
|
|
|
|
if language and translations:
|
|
translations = self.pagecontent_set(manager="admin_manager").filter(language=language)
|
|
elif translations:
|
|
translations = self.pagecontent_set(manager="admin_manager")
|
|
else:
|
|
translations = self.pagecontent_set(manager="admin_manager").none()
|
|
translations = translations.prefetch_related("placeholders")
|
|
|
|
# copy titles of this page
|
|
for title in translations.current_content():
|
|
new_title = model_to_dict(title)
|
|
new_title.pop("id", None) # No PK
|
|
new_title["page"] = new_page
|
|
|
|
if parent_page:
|
|
base = parent_page.get_path(title.language)
|
|
path = f"{base}/{title.slug}" if base else title.slug
|
|
else:
|
|
base = ""
|
|
path = title.slug
|
|
|
|
new_title["slug"] = get_available_slug(site, path, title.language)
|
|
if title.overwrite_url:
|
|
new_title["overwrite_url"] = "{}/{}".format(base, new_title["slug"]) if base else new_title["slug"]
|
|
new_title = PageContent.objects.with_user(user).create(**new_title)
|
|
if new_title.is_public():
|
|
# The page URL is derived from public content only. With a
|
|
# versioning package installed the copy is created as a draft
|
|
# and gets its URL on first publish.
|
|
new_page.update_urls_from_content(new_title.language)
|
|
|
|
for placeholder in title.placeholders.all():
|
|
# copy the placeholders (and plugins on those placeholders!)
|
|
new_placeholder = new_title.placeholders.create(
|
|
slot=placeholder.slot,
|
|
default_width=placeholder.default_width,
|
|
)
|
|
placeholder.copy_plugins(new_placeholder, language=new_title.language)
|
|
new_page.page_content_cache[new_title.language] = new_title
|
|
|
|
if extensions:
|
|
from cms.extensions import extension_pool
|
|
|
|
extension_pool.copy_extensions(self, new_page)
|
|
|
|
# copy permissions if requested
|
|
if permissions and get_cms_setting("PERMISSION"):
|
|
permissions = self.pagepermission_set.iterator()
|
|
permissions_new = []
|
|
|
|
for permission in permissions:
|
|
permission.pk = None
|
|
permission.page = new_page
|
|
permissions_new.append(permission)
|
|
|
|
if permissions_new:
|
|
from cms.cache.permissions import clear_permission_cache
|
|
from cms.utils.permissions import clear_permission_lru_caches
|
|
|
|
new_page.pagepermission_set.bulk_create(permissions_new)
|
|
# bulk_create does not emit the permission signals.
|
|
clear_permission_cache()
|
|
clear_permission_lru_caches(user)
|
|
# A concurrent request can rebuild the cache before these rows commit.
|
|
transaction.on_commit(clear_permission_cache)
|
|
return new_page
|
|
|
|
def copy_with_descendants(
|
|
self, target_page=None, target_node=None, position=None, copy_permissions=True, target_site=None, user=None
|
|
):
|
|
"""
|
|
Copy a page [ and all its descendants to a new location ]
|
|
"""
|
|
from cms.models import PageContent
|
|
|
|
if target_node is not None:
|
|
warnings.warn(
|
|
"Argument `parent_node` is deprecated. Use `parent_page` instead.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
target_page = target_page or target_node
|
|
|
|
if position in ("first-child", "last-child"):
|
|
parent_page = target_page
|
|
elif target_page:
|
|
parent_page = target_page.parent
|
|
else:
|
|
parent_page = None
|
|
|
|
if target_site is None:
|
|
target_site = parent_page.site if parent_page else self.site
|
|
|
|
# Evaluate the descendants queryset BEFORE copying the page.
|
|
# Otherwise, if the page is copied and pasted on itself, it will duplicate.
|
|
descendants = list(
|
|
self.get_descendant_pages().prefetch_related(
|
|
"urls",
|
|
Prefetch("pagecontent_set", queryset=PageContent.admin_manager.all()),
|
|
)
|
|
)
|
|
if copy_permissions:
|
|
# Snapshot paths before inserting the copy can move source nodes.
|
|
inherited_restrictions = self.get_view_restrictions(inherited_only=True)
|
|
new_root_page = self.copy(target_site, parent_page=parent_page, permissions=copy_permissions, user=user)
|
|
|
|
if target_page and position in ("first-child"):
|
|
# target page is a parent and user has requested to
|
|
# insert the new page as its first child
|
|
new_root_page.move(target_page, position)
|
|
new_root_page.refresh_from_db(fields=("path", "depth"))
|
|
|
|
if target_page and position in ("left", "last-child"):
|
|
# target page is a sibling
|
|
new_root_page.move(target_page, position)
|
|
new_root_page.refresh_from_db(fields=("path", "depth"))
|
|
|
|
pages_by_id = {self.id: new_root_page}
|
|
for page in descendants:
|
|
parent = pages_by_id[page.parent_id]
|
|
pages_by_id[page.id] = page.copy(
|
|
target_site, parent_page=parent, translations=True, permissions=copy_permissions, user=user
|
|
)
|
|
|
|
if copy_permissions:
|
|
# Ancestors outside the copied subtree will no longer protect it.
|
|
new_root_page.apply_view_restrictions(inherited_restrictions, user=user)
|
|
return new_root_page
|
|
|
|
def delete(self, *args, **kwargs):
|
|
using = self._state.db or router.db_for_write(Page, instance=self)
|
|
Page.get_tree(self).using(using).delete_fast()
|
|
|
|
if self.parent_id:
|
|
Page.objects.using(using).filter(id=self.parent_id).update(
|
|
numchild=models.F("numchild") - 1
|
|
)
|
|
self.clear_cache(menu=True)
|
|
|
|
def delete_translations(self, language=None):
|
|
if language is None:
|
|
languages = self.get_languages()
|
|
else:
|
|
languages = [language]
|
|
|
|
self.pagecontent_set(manager="admin_manager").filter(language__in=languages).delete()
|
|
|
|
def save(self, **kwargs):
|
|
if self.reverse_id == "":
|
|
self.reverse_id = None
|
|
if self.application_namespace == "":
|
|
self.application_namespace = None
|
|
|
|
created = not bool(self.pk)
|
|
from cms.utils.permissions import get_current_user_name
|
|
|
|
self.changed_by = get_current_user_name()
|
|
if created:
|
|
self.created_by = self.changed_by
|
|
|
|
super().save(**kwargs)
|
|
|
|
def update(self, refresh=False, **data):
|
|
cls = self.__class__
|
|
cls.objects.filter(pk=self.pk).update(**data)
|
|
|
|
if refresh:
|
|
return self.reload()
|
|
else:
|
|
for field, value in data.items():
|
|
setattr(self, field, value)
|
|
return
|
|
|
|
def update_translations(self, language=None, **data):
|
|
if language:
|
|
translations = self.pagecontent_set.filter(language=language)
|
|
else:
|
|
translations = self.pagecontent_set.all()
|
|
return translations.update(**data)
|
|
|
|
def has_translation(self, language):
|
|
return self.pagecontent_set.filter(language=language).exists()
|
|
|
|
def clear_cache(self, language=None, menu=False, placeholder=False):
|
|
from cms.cache import invalidate_cms_page_cache
|
|
|
|
if get_cms_setting("PAGE_CACHE"):
|
|
# Clears all the page caches
|
|
invalidate_cms_page_cache()
|
|
|
|
if placeholder and get_cms_setting("PLACEHOLDER_CACHE"):
|
|
assert language, "language is required when clearing placeholder cache"
|
|
|
|
placeholders = self.get_placeholders(language)
|
|
|
|
for placeholder_instance in placeholders:
|
|
placeholder_instance.clear_cache(language, site_id=self.site_id)
|
|
|
|
if menu:
|
|
# Clears all menu caches for this page's site
|
|
menu_pool.clear(site_id=self.site_id)
|
|
|
|
def get_child_pages(self):
|
|
return self.get_children().order_by("path")
|
|
|
|
def get_ancestor_pages(self):
|
|
return self.get_ancestors().order_by("path")
|
|
|
|
def get_descendant_pages(self):
|
|
return self.get_descendants().order_by("path")
|
|
|
|
def get_root(self):
|
|
return self.__class__.objects.using(self._state.db).get(
|
|
path=self.path[0 : self.steplen]
|
|
)
|
|
|
|
def get_parent_page(self):
|
|
warnings.warn(
|
|
"Method `get_parent_page()` is deprecated. Instead use the `parent` attribute.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
return self.parent
|
|
|
|
@property
|
|
def languages(self):
|
|
warnings.warn(
|
|
"Attribute `languages` is deprecated. Use `get_languages` instead.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
return ",".join(self.get_languages())
|
|
|
|
def get_languages(self, admin_manager=True):
|
|
"""Returns available languages for the page. This is potentially costly."""
|
|
if admin_manager:
|
|
if self.admin_content_cache is None:
|
|
self.set_admin_content_cache()
|
|
return list(self.admin_content_cache.keys())
|
|
if not self.page_content_cache:
|
|
self._get_page_content_cache(get_language(), fallback=False, force_reload=False)
|
|
return list(self.page_content_cache.keys())
|
|
|
|
def remove_language(self, language):
|
|
warnings.warn(
|
|
"Method `remove_language` is deprecated and has no effect any more.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
def update_languages(self, languages):
|
|
warnings.warn(
|
|
"Method `update_languages` is deprecated and has no effect any more.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
def get_published_languages(self):
|
|
warnings.warn(
|
|
"Method `get_published_languages` is deprecated. Use `get_languages(admin_manager=False)` instead.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
return self.get_languages(admin_manager=False)
|
|
|
|
def set_translations_cache(self):
|
|
warnings.warn(
|
|
"Method `set_translations_cache` is deprecated. Use `get_content_obj` instead. "
|
|
"For admin views use `set_admin_content_cache` instead.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
for translation in self.pagecontent_set.all():
|
|
self.page_content_cache.setdefault(translation.language, translation)
|
|
|
|
def set_admin_content_cache(self):
|
|
self.admin_content_cache = AdminCacheDict()
|
|
for translation in self.pagecontent_set(manager="admin_manager").latest_content().all():
|
|
self.admin_content_cache.setdefault(translation.language, translation)
|
|
|
|
def get_admin_content(self, language, fallback=False):
|
|
from cms.models.contentmodels import EmptyPageContent
|
|
|
|
if self.admin_content_cache is None:
|
|
self.set_admin_content_cache()
|
|
page_content = self.admin_content_cache.get(language, EmptyPageContent(language=language, page=self))
|
|
if not page_content and fallback:
|
|
for lang in i18n.get_fallback_languages(language, site_id=self.site_id):
|
|
page_content = self.admin_content_cache.get(lang)
|
|
if page_content:
|
|
return page_content
|
|
page_content = EmptyPageContent(language=language, page=self)
|
|
if fallback == "force":
|
|
# Try any page content object
|
|
for item in self.admin_content_cache.values():
|
|
if item:
|
|
return item
|
|
return page_content
|
|
|
|
def get_path_for_slug(self, slug, language):
|
|
if self.is_home:
|
|
return ""
|
|
|
|
if self.parent:
|
|
if self.parent.is_home:
|
|
# children of the home page live directly at the root
|
|
path = slug
|
|
else:
|
|
base = self.parent.get_path(language, fallback=True)
|
|
# a non-home parent without a path (e.g. unpublished) means
|
|
# this page has no reachable path either
|
|
path = f"{base}/{slug}" if base else None
|
|
else:
|
|
path = slug
|
|
return path
|
|
|
|
def get_url_data(self, slug, overwrite_url, language):
|
|
"""Derive the :class:`~cms.models.pagemodel.PageUrl` fields (``slug``,
|
|
``path``, ``managed``) from an authored slug and overwrite URL."""
|
|
if overwrite_url and not self.is_home:
|
|
path = overwrite_url.strip("/")
|
|
else:
|
|
# the home page always lives at the root path
|
|
path = self.get_path_for_slug(slug, language)
|
|
return {
|
|
"slug": slug,
|
|
"path": path,
|
|
"managed": not bool(overwrite_url),
|
|
}
|
|
|
|
def get_url(self, language):
|
|
return self.get_urls().get(language=language)
|
|
|
|
def get_urls(self):
|
|
return self.urls.all()
|
|
|
|
def update_urls(self, language=None, **data):
|
|
if language:
|
|
page_urls_qs = self.get_urls().filter(language=language)
|
|
else:
|
|
page_urls_qs = self.get_urls().all()
|
|
|
|
if "path" in data and data["path"] is not None:
|
|
new_path = data["path"]
|
|
languages_to_check = (
|
|
[language] if language else list(page_urls_qs.values_list("language", flat=True).distinct())
|
|
)
|
|
|
|
for lang in languages_to_check:
|
|
# Check if the new path exists for this language on another page on the same site
|
|
existing_url_collision = (
|
|
PageUrl.objects.filter(
|
|
language=lang,
|
|
path=new_path,
|
|
site=self.site_id,
|
|
)
|
|
.exclude(pk__in=page_urls_qs.filter(language=lang).values("pk"))
|
|
.exists()
|
|
)
|
|
|
|
if existing_url_collision:
|
|
page_pks_being_updated = list(
|
|
page_urls_qs.filter(language=lang).values_list("page__pk", flat=True)
|
|
)
|
|
raise IntegrityError(
|
|
f"Cannot update URL path. A page URL with path='{new_path}' "
|
|
f"and language='{lang}' already exists for another page "
|
|
f"on the same site (not page(s): {page_pks_being_updated})."
|
|
)
|
|
|
|
# If no collision detected, proceed with the update
|
|
return page_urls_qs.update(**data)
|
|
|
|
def update_urls_from_content(self, language):
|
|
"""Derive this page's :class:`~cms.models.pagemodel.PageUrl` for ``language``
|
|
from the publicly visible :class:`~cms.models.contentmodels.PageContent`
|
|
object, then update the paths of all descendant pages.
|
|
|
|
The slug and overwrite URL are authored on the page content object.
|
|
``PageUrl`` is a routing table that must only ever reflect published
|
|
content: without a versioning package every saved content is public and
|
|
this runs on every save; with a versioning package installed it must be
|
|
called when a version is published or unpublished.
|
|
|
|
When no public content exists, the URL's path is set to ``None`` so the
|
|
page stops resolving while the slug stays reserved for the page.
|
|
|
|
Raises ``IntegrityError`` if the derived path collides with the URL of
|
|
another page on the same site.
|
|
"""
|
|
from cms.models import PageContent
|
|
|
|
content = PageContent.objects.filter(page=self, language=language).first()
|
|
url = self.urls.filter(language=language).first()
|
|
|
|
if content is None:
|
|
if url is None or url.path is None:
|
|
return
|
|
self.urls.filter(language=language).update(path=None)
|
|
else:
|
|
data = self.get_url_data(content.slug, content.overwrite_url, language)
|
|
path = data["path"]
|
|
if url is not None:
|
|
if (url.slug, url.path, url.managed) == (data["slug"], data["path"], data["managed"]):
|
|
# the URL already reflects the content; descendant paths derive
|
|
# from this page's path, so they are up to date as well
|
|
return
|
|
self.update_urls(language, **data) # includes the path collision check
|
|
else:
|
|
collision = (
|
|
path is not None
|
|
and PageUrl.objects.filter(language=language, path=path, site=self.site_id).exists()
|
|
)
|
|
if collision:
|
|
raise IntegrityError(
|
|
f"Cannot create URL. A page URL with path='{path}' and "
|
|
f"language='{language}' already exists for another page on the same site."
|
|
)
|
|
self.urls.create(page=self, site_id=self.site_id, language=language, **data)
|
|
|
|
self.urls_cache = {}
|
|
self._update_url_path_recursive(language)
|
|
|
|
def get_fallbacks(self, language):
|
|
return i18n.get_fallback_languages(language, site_id=self.site_id)
|
|
|
|
# ## PageContent object access
|
|
|
|
def get_content_obj(self, language=None, fallback=True, force_reload=False):
|
|
"""Helper function for accessing wanted / current title.
|
|
If wanted title doesn't exist, EmptyPageContent instance will be returned.
|
|
"""
|
|
language = self._get_page_content_cache(language, fallback, force_reload)
|
|
if language in self.page_content_cache:
|
|
return self.page_content_cache[language]
|
|
from cms.models import EmptyPageContent
|
|
|
|
return EmptyPageContent(language=language, page=self)
|
|
|
|
def get_page_content_obj_attribute(self, attrname, language=None, fallback=True, force_reload=False):
|
|
"""Helper function for getting attribute or None from wanted/current page content."""
|
|
try:
|
|
attribute = getattr(self.get_content_obj(language, fallback, force_reload), attrname)
|
|
return attribute
|
|
except AttributeError:
|
|
return None
|
|
|
|
def get_url_obj(self, language, fallback=True):
|
|
"""Get the path of the page depending on the given language"""
|
|
languages = [language]
|
|
if fallback:
|
|
languages.extend(self.get_fallbacks(language))
|
|
|
|
if self.urls_cache is None or language not in self.urls_cache:
|
|
# `get_page_from_request` will fill the cache only for the current language
|
|
# Here, we fully fill it and try again
|
|
self.urls_cache = {url.language: url for url in self.urls.all() if url.language in languages}
|
|
|
|
return next((self.urls_cache[lang] for lang in languages if lang in self.urls_cache), None)
|
|
|
|
def get_path(self, language, fallback=True):
|
|
url = self.get_url_obj(language, fallback)
|
|
if url:
|
|
return url.path
|
|
return None
|
|
|
|
def get_slug(self, language, fallback=True):
|
|
url = self.get_url_obj(language, fallback)
|
|
if url:
|
|
return url.slug
|
|
return None
|
|
|
|
def get_title(self, language=None, fallback=True, force_reload=False):
|
|
"""
|
|
get the title of the page depending on the given language
|
|
"""
|
|
return self.get_page_content_obj_attribute("title", language, fallback, force_reload)
|
|
|
|
def get_menu_title(self, language=None, fallback=True, force_reload=False):
|
|
"""
|
|
get the menu title of the page depending on the given language
|
|
"""
|
|
menu_title = self.get_page_content_obj_attribute("menu_title", language, fallback, force_reload)
|
|
if not menu_title:
|
|
return self.get_title(language, True, force_reload)
|
|
return menu_title
|
|
|
|
def get_placeholders(self, language: str, admin_manager: bool = False) -> models.QuerySet:
|
|
from cms.models import PageContent, Placeholder
|
|
|
|
if admin_manager:
|
|
# Use the admin manager to get the current content (i.e. potentially not yet published content)
|
|
page_content = PageContent.admin_manager.current_content().get(language=language, page=self)
|
|
else:
|
|
page_content = PageContent.objects.get(language=language, page=self)
|
|
return Placeholder.objects.get_for_obj(page_content)
|
|
|
|
def get_changed_date(self, language=None, fallback=True, force_reload=False):
|
|
"""
|
|
get when this page was last updated
|
|
"""
|
|
return self.get_page_content_obj_attribute("changed_date", language, fallback, force_reload)
|
|
|
|
def get_changed_by(self, language=None, fallback=True, force_reload=False):
|
|
"""
|
|
get user who last changed this page
|
|
"""
|
|
return self.get_page_content_obj_attribute("changed_by", language, fallback, force_reload)
|
|
|
|
def get_page_title(self, language=None, fallback=True, force_reload=False):
|
|
"""
|
|
get the page title of the page depending on the given language
|
|
"""
|
|
page_title = self.get_page_content_obj_attribute("page_title", language, fallback, force_reload)
|
|
|
|
if not page_title:
|
|
return self.get_title(language, True, force_reload)
|
|
return page_title
|
|
|
|
def get_meta_description(self, language=None, fallback=True, force_reload=False):
|
|
"""
|
|
get content for the description meta tag for the page depending on the given language
|
|
"""
|
|
return self.get_page_content_obj_attribute("meta_description", language, fallback, force_reload)
|
|
|
|
def get_application_urls(self, language=None, fallback=True, force_reload=False):
|
|
"""
|
|
get application urls conf for application hook
|
|
"""
|
|
return self.application_urls
|
|
|
|
def get_redirect(self, language=None, fallback=True, force_reload=False):
|
|
"""
|
|
get redirect
|
|
"""
|
|
return self.get_page_content_obj_attribute("redirect", language, fallback, force_reload)
|
|
|
|
def _get_page_content_cache(self, language, fallback, force_reload):
|
|
"""
|
|
Sets the internal page object cache for page content objects available to the general user.
|
|
It optionally respe
|
|
"""
|
|
|
|
def get_fallback_language(page, language):
|
|
fallback_langs = i18n.get_fallback_languages(language, site_id=page.site_id)
|
|
for lang in fallback_langs:
|
|
if page.page_content_cache.get(lang):
|
|
return lang
|
|
|
|
if not language:
|
|
language = get_language()
|
|
|
|
# Update page_content_cache from _prefetched_objects_cache if available
|
|
prefetch_cache = getattr(self, "_prefetched_objects_cache", {})
|
|
cached_page_content = prefetch_cache.get("pagecontent_set", [])
|
|
for page_content in cached_page_content:
|
|
self.page_content_cache[page_content.language] = page_content
|
|
|
|
# Reload if explicitly needed or language not in content cache
|
|
if force_reload or language not in self.page_content_cache:
|
|
for page_content in self.pagecontent_set.all():
|
|
self.page_content_cache[page_content.language] = page_content
|
|
|
|
if self.page_content_cache.get(language):
|
|
return language
|
|
|
|
use_fallback = all(
|
|
[fallback, not self.page_content_cache.get(language), get_fallback_language(self, language)]
|
|
)
|
|
if use_fallback:
|
|
# language can be in the cache but might be an EmptyPageContent instance
|
|
return get_fallback_language(self, language)
|
|
return language
|
|
|
|
@property
|
|
def template(self):
|
|
return self.get_page_content_obj_attribute("template")
|
|
|
|
@property
|
|
def soft_root(self):
|
|
return self.get_page_content_obj_attribute("soft_root")
|
|
|
|
def get_template(self, language=None, fallback=True, force_reload=False):
|
|
content = self.get_content_obj(language, fallback, force_reload)
|
|
if content:
|
|
return content.get_template(app_hooks=False)
|
|
return get_cms_setting("TEMPLATES")[0][0] if get_cms_setting("TEMPLATES") else ""
|
|
|
|
def get_template_name(self):
|
|
"""
|
|
get the textual name (2nd parameter in get_cms_setting('TEMPLATES'))
|
|
of the template of this page or of the nearest
|
|
ancestor. failing to find that, return the name of the default template.
|
|
"""
|
|
template = self.get_template()
|
|
for t in get_cms_setting("TEMPLATES"):
|
|
if t[0] == template:
|
|
return t[1]
|
|
return _("default")
|
|
|
|
def has_view_permission(self, user):
|
|
from cms.utils.page_permissions import user_can_view_page
|
|
|
|
return user_can_view_page(user, page=self)
|
|
|
|
def has_view_restrictions(self, site):
|
|
from cms.models import PagePermission
|
|
|
|
if get_cms_setting("PERMISSION"):
|
|
restrictions = PagePermission.objects.for_page(self).filter(can_view=True)
|
|
return restrictions.exists()
|
|
return False
|
|
|
|
def has_add_permission(self, user):
|
|
"""
|
|
Has user ability to add page under current page?
|
|
"""
|
|
from cms.utils.page_permissions import user_can_add_subpage
|
|
|
|
return user_can_add_subpage(user, self)
|
|
|
|
def has_change_permission(self, user):
|
|
from cms.utils.page_permissions import user_can_change_page
|
|
|
|
return user_can_change_page(user, page=self)
|
|
|
|
def has_delete_permission(self, user):
|
|
from cms.utils.page_permissions import user_can_delete_page
|
|
|
|
return user_can_delete_page(user, page=self)
|
|
|
|
def has_delete_translation_permission(self, user, language):
|
|
from cms.utils.page_permissions import user_can_delete_page_translation
|
|
|
|
return user_can_delete_page_translation(user, page=self, language=language)
|
|
|
|
def has_publish_permission(self, user):
|
|
from cms.utils.page_permissions import user_can_publish_page
|
|
|
|
return user_can_publish_page(user, page=self)
|
|
|
|
def has_advanced_settings_permission(self, user):
|
|
from cms.utils.page_permissions import user_can_change_page_advanced_settings
|
|
|
|
return user_can_change_page_advanced_settings(user, page=self)
|
|
|
|
def has_change_permissions_permission(self, user):
|
|
"""
|
|
Has user ability to change permissions for current page?
|
|
"""
|
|
from cms.utils.page_permissions import user_can_change_page_permissions
|
|
|
|
return user_can_change_page_permissions(user, page=self)
|
|
|
|
def has_move_page_permission(self, user):
|
|
"""Has user ability to move current page?"""
|
|
from cms.utils.page_permissions import user_can_move_page
|
|
|
|
return user_can_move_page(user, page=self)
|
|
|
|
def get_media_path(self, filename):
|
|
"""
|
|
Returns path (relative to MEDIA_ROOT/MEDIA_URL) to directory for storing
|
|
page-scope files. This allows multiple pages to contain files with
|
|
identical names without namespace issues. Plugins such as Picture can
|
|
use this method to initialise the 'upload_to' parameter for File-based
|
|
fields. For example:
|
|
image = models.ImageField(
|
|
_("image"), upload_to=CMSPlugin.get_media_path)
|
|
|
|
where CMSPlugin.get_media_path calls self.page.get_media_path
|
|
|
|
This location can be customised using the CMS_PAGE_MEDIA_PATH setting
|
|
"""
|
|
return join(get_cms_setting("PAGE_MEDIA_PATH"), "%d" % self.pk, filename)
|
|
|
|
def reload(self):
|
|
"""
|
|
Reload a page from the database
|
|
"""
|
|
return self.__class__.objects.get(pk=self.pk)
|
|
|
|
def rescan_placeholders(self, language):
|
|
return self.get_content_obj(language=language).rescan_placeholders()
|
|
|
|
def get_declared_placeholders(self, language=None, fallback=True, force_reload=False):
|
|
from cms.utils.placeholder import get_declared_placeholders_for_obj
|
|
|
|
content = self.get_content_obj(language, fallback, force_reload)
|
|
if content:
|
|
return get_declared_placeholders_for_obj(content)
|
|
return []
|
|
|
|
def get_xframe_options(self, language=None, fallback=True, force_reload=False):
|
|
content = self.get_content_obj(language, fallback, force_reload)
|
|
if content:
|
|
return content.get_xframe_options()
|
|
|
|
def get_soft_root(self, language=None, fallback=True, force_reload=False):
|
|
return self.get_page_content_obj_attribute("soft_root", language, fallback, force_reload)
|
|
|
|
def get_in_navigation(self, language=None, fallback=True, force_reload=False):
|
|
return self.get_page_content_obj_attribute("in_navigation", language, fallback, force_reload)
|
|
|
|
def get_limit_visibility_in_menu(self, language=None, fallback=True, force_reload=False):
|
|
return self.get_page_content_obj_attribute("limit_visibility_in_menu", language, fallback, force_reload)
|
|
|
|
|
|
class PageUrl(models.Model):
|
|
slug = models.SlugField(_("slug"), max_length=255, db_index=True)
|
|
path = models.CharField(_("Path"), max_length=255, db_index=True, null=True)
|
|
language = models.CharField(_("language"), max_length=15, db_index=True)
|
|
page = models.ForeignKey(
|
|
Page,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("page"),
|
|
related_name="urls",
|
|
)
|
|
#: Denormalized from ``page.site`` so path uniqueness per site can be
|
|
#: enforced by the database.
|
|
site = models.ForeignKey(
|
|
Site,
|
|
on_delete=models.CASCADE,
|
|
verbose_name=_("site"),
|
|
related_name="djangocms_urls",
|
|
editable=False,
|
|
)
|
|
managed = models.BooleanField(default=False)
|
|
objects = PageUrlManager()
|
|
|
|
class Meta:
|
|
app_label = "cms"
|
|
default_permissions = []
|
|
constraints = [
|
|
UniqueConstraint(fields=["page", "language"], name="unique_together_page_language"),
|
|
# NULL paths (unreachable pages) are distinct on all supported
|
|
# backends, so any number of them may coexist.
|
|
UniqueConstraint(fields=["site", "language", "path"], name="unique_site_language_path"),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.path or self.slug} ({self.language})"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.site_id is None:
|
|
self.site_id = self.page.site_id
|
|
super().save(*args, **kwargs)
|
|
|
|
def get_absolute_url(self, language=None, fallback=True):
|
|
if not language:
|
|
language = get_current_language()
|
|
|
|
with force_language(language):
|
|
try:
|
|
if self.path == "":
|
|
return reverse("pages-root")
|
|
return reverse("pages-details-by-slug", kwargs={"slug": self.path})
|
|
except NoReverseMatch:
|
|
return None
|
|
|
|
def get_path_for_base(self, base_path=""):
|
|
old_base, sep, slug = self.path.rpartition("/")
|
|
return f"{base_path}/{slug}" if base_path else slug
|
|
|
|
|
|
class PageType(Page):
|
|
class Meta:
|
|
proxy = True
|
|
default_permissions = []
|
|
|
|
@classmethod
|
|
def get_root_page(cls, site):
|
|
pages = Page.objects.on_site(site).filter(
|
|
depth=1,
|
|
is_page_type=True,
|
|
)
|
|
return pages.first()
|
|
|
|
def is_potential_home(self):
|
|
return False
|