forked from ungleich-public/dynamicweb
418 lines
15 KiB
Python
418 lines
15 KiB
Python
from django.db import models
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from cms import constants
|
|
from cms.models.fields import PlaceholderRelationField
|
|
from cms.models.managers import ContentAdminManager, PageContentManager
|
|
from cms.models.pagemodel import Page
|
|
from cms.utils.conf import get_cms_setting
|
|
|
|
|
|
class PageContent(models.Model):
|
|
# Field changes need a custom migration operation as in 0044: versioning packages drop the
|
|
# unique_together below in the database only, and SQLite rebuilds the table from the state.
|
|
LIMIT_VISIBILITY_IN_MENU_CHOICES = (
|
|
(constants.VISIBILITY_ALL, _("no limit set")),
|
|
(constants.VISIBILITY_USERS, _("for logged in users only")),
|
|
(constants.VISIBILITY_ANONYMOUS, _("for anonymous users only")),
|
|
)
|
|
|
|
template_choices = get_cms_setting("TEMPLATES")
|
|
|
|
TEMPLATE_DEFAULT = (
|
|
constants.TEMPLATE_INHERITANCE_MAGIC
|
|
if get_cms_setting("TEMPLATE_INHERITANCE")
|
|
else (template_choices[0][0] if template_choices else "")
|
|
)
|
|
|
|
X_FRAME_OPTIONS_CHOICES = (
|
|
(constants.X_FRAME_OPTIONS_INHERIT, _("Inherit from parent page")),
|
|
(constants.X_FRAME_OPTIONS_DENY, _("Deny")),
|
|
(constants.X_FRAME_OPTIONS_SAMEORIGIN, _("Only this website")),
|
|
(constants.X_FRAME_OPTIONS_ALLOW, _("Allow")),
|
|
)
|
|
|
|
# These are the fields whose values are compared when saving
|
|
# a PageContent object to know if it has changed.
|
|
editable_fields = [
|
|
"title",
|
|
"slug",
|
|
"overwrite_url",
|
|
"redirect",
|
|
"page_title",
|
|
"menu_title",
|
|
"meta_description",
|
|
]
|
|
|
|
language = models.CharField(_("language"), max_length=15, db_index=True)
|
|
title = models.CharField(
|
|
verbose_name=_("title"),
|
|
max_length=255,
|
|
help_text=_("The default title"),
|
|
)
|
|
page_title = models.CharField(
|
|
verbose_name=_("Page Title"),
|
|
max_length=255,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_("Overwrites what is displayed at the top of your browser or in bookmarks"),
|
|
)
|
|
menu_title = models.CharField(
|
|
verbose_name=_("Menu Title"),
|
|
max_length=255,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_("Overwrite what is displayed in the menu"),
|
|
)
|
|
meta_description = models.TextField(
|
|
verbose_name=_("Description meta tag"),
|
|
blank=True,
|
|
null=True,
|
|
help_text=_("A description of the page used by search engines."),
|
|
)
|
|
redirect = models.CharField(
|
|
verbose_name=_("redirect"),
|
|
max_length=2048,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_("Redirects to this URL."),
|
|
)
|
|
#: The slug and overwrite URL are authored per page content object. The routable
|
|
#: :class:`~cms.models.pagemodel.PageUrl` objects are derived from the publicly
|
|
#: visible page content only (see :meth:`cms.models.pagemodel.Page.update_urls_from_content`),
|
|
#: so changing the slug of a draft version does not affect the published URL.
|
|
slug = models.SlugField(
|
|
verbose_name=_("slug"),
|
|
max_length=255,
|
|
help_text=_("The part of the title that is used in the URL"),
|
|
)
|
|
overwrite_url = models.CharField(
|
|
verbose_name=_("overwrite URL"),
|
|
max_length=255,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_("Keep this field empty if standard path should be used."),
|
|
)
|
|
page = models.ForeignKey(Page, on_delete=models.CASCADE, verbose_name=_("page"), related_name="pagecontent_set")
|
|
creation_date = models.DateTimeField(verbose_name=_("creation date"), editable=False, default=timezone.now)
|
|
# Placeholders (plugins)
|
|
placeholders = PlaceholderRelationField()
|
|
|
|
created_by = models.CharField(
|
|
verbose_name=_("created by"),
|
|
max_length=constants.PAGE_USERNAME_MAX_LENGTH,
|
|
editable=False,
|
|
)
|
|
changed_by = models.CharField(
|
|
verbose_name=_("changed by"),
|
|
max_length=constants.PAGE_USERNAME_MAX_LENGTH,
|
|
editable=False,
|
|
)
|
|
changed_date = models.DateTimeField(auto_now=True)
|
|
|
|
in_navigation = models.BooleanField(
|
|
verbose_name=_("in navigation"),
|
|
default=True,
|
|
db_index=True,
|
|
)
|
|
soft_root = models.BooleanField(
|
|
verbose_name=_("soft root"),
|
|
db_index=True,
|
|
default=False,
|
|
help_text=_("All ancestors will not be displayed in the navigation"),
|
|
)
|
|
template = models.CharField(
|
|
verbose_name=_("template"),
|
|
max_length=100,
|
|
choices=template_choices,
|
|
help_text=_("The template used to render the content."),
|
|
default=TEMPLATE_DEFAULT,
|
|
)
|
|
limit_visibility_in_menu = models.SmallIntegerField(
|
|
verbose_name=_("menu visibility"),
|
|
default=constants.VISIBILITY_ALL,
|
|
choices=LIMIT_VISIBILITY_IN_MENU_CHOICES,
|
|
db_index=True,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_("limit when this page is visible in the menu"),
|
|
)
|
|
|
|
# X Frame Options for clickjacking protection
|
|
xframe_options = models.IntegerField(
|
|
choices=X_FRAME_OPTIONS_CHOICES,
|
|
default=get_cms_setting("DEFAULT_X_FRAME_OPTIONS"),
|
|
verbose_name=_("X Frame Options"),
|
|
)
|
|
|
|
objects = PageContentManager()
|
|
admin_manager = ContentAdminManager()
|
|
"""Admin_manager does lack additional functionality of objects and must only
|
|
be used inside admin objects or admin forms. One of its key properties is that
|
|
it can access all objects of type PageContent (irrespevtively of some objects
|
|
being hidden by third-party packages, e.g. due to viewing rights, publication
|
|
or moderation status.
|
|
"""
|
|
|
|
class Meta:
|
|
verbose_name = _("page content")
|
|
verbose_name_plural = _("page contents")
|
|
default_permissions = []
|
|
unique_together = (("language", "page"),)
|
|
# TODO: unique_together shall be replaced against:
|
|
# constraints = [
|
|
# models.UniqueConstraint(fields=['language', 'page'], name='language_and_page_uniq')
|
|
# ]
|
|
app_label = "cms"
|
|
|
|
def __str__(self):
|
|
return f"{self.title} ({self.language})"
|
|
|
|
def __repr__(self):
|
|
display = f"<{self.__module__}.{self.__class__.__name__} id={self.pk} object at {hex(id(self))}>"
|
|
return display
|
|
|
|
def update(self, **data):
|
|
for field, value in data.items():
|
|
setattr(self, field, value)
|
|
self.save(update_fields=data.keys())
|
|
|
|
def save(self, **kwargs):
|
|
# delete template cache
|
|
if hasattr(self, "_template_cache"):
|
|
delattr(self, "_template_cache")
|
|
super().save(**kwargs)
|
|
|
|
def toggle_in_navigation(self, set_to=None):
|
|
"""
|
|
Toggles (or sets) in_navigation and invalidates the cms page cache
|
|
"""
|
|
old = bool(self.in_navigation)
|
|
|
|
if set_to in [True, False]:
|
|
new = set_to
|
|
else:
|
|
new = not old
|
|
|
|
self.update(in_navigation=new)
|
|
|
|
# If there was a change, invalidate the cms page cache
|
|
if new != old:
|
|
self.page.clear_cache()
|
|
return new
|
|
|
|
def has_placeholder_change_permission(self, user):
|
|
return self.page.has_change_permission(user)
|
|
|
|
def has_placeholder_view_permission(self, user):
|
|
return self.page.has_view_permission(user)
|
|
|
|
def has_publish_permission(self, user):
|
|
return self.page.has_publish_permission(user)
|
|
|
|
def rescan_placeholders(self):
|
|
"""
|
|
Rescan and if necessary create placeholders in the current template.
|
|
"""
|
|
from cms.utils.placeholder import rescan_placeholders_for_obj
|
|
|
|
return rescan_placeholders_for_obj(self)
|
|
|
|
def get_placeholders(self):
|
|
if not hasattr(self, "_placeholder_cache"):
|
|
self._placeholder_cache = self.placeholders.all()
|
|
return self._placeholder_cache
|
|
|
|
def get_ancestor_titles(self):
|
|
return PageContent.objects.filter(
|
|
page__in=self.page.get_ancestor_pages(),
|
|
language=self.language,
|
|
)
|
|
|
|
def get_placeholder_slots(self):
|
|
"""
|
|
Returns a list of placeholder slots for this page content object.
|
|
"""
|
|
if not get_cms_setting("PLACEHOLDERS"):
|
|
return []
|
|
if not hasattr(self, "_placeholder_slot_cache"):
|
|
if self.template == constants.TEMPLATE_INHERITANCE_MAGIC:
|
|
templates = (
|
|
self.get_ancestor_titles()
|
|
.exclude(template=constants.TEMPLATE_INHERITANCE_MAGIC)
|
|
.order_by("-page__path")
|
|
.values_list("template", flat=True)
|
|
)
|
|
if templates:
|
|
placeholder_set = templates[0]
|
|
else:
|
|
placeholder_set = get_cms_setting("PLACEHOLDERS")[0][0]
|
|
else:
|
|
placeholder_set = self.template or get_cms_setting("PLACEHOLDERS")[0][0]
|
|
|
|
for key, value, __ in get_cms_setting("PLACEHOLDERS"):
|
|
if key in (placeholder_set, ""):
|
|
# - Empty string matches always
|
|
self._placeholder_slot_cache = value
|
|
break
|
|
else: # No matching placeholder list found
|
|
self._placeholder_slot_cache = get_cms_setting("PLACEHOLDERS")[0][1]
|
|
if isinstance(self._placeholder_slot_cache, str):
|
|
# Accidentally a strong not a tuple? Make it a 1-element tuple
|
|
self._placeholder_slot_cache = (self._placeholder_slot_cache,)
|
|
return self._placeholder_slot_cache
|
|
|
|
def get_template(self, app_hooks=True):
|
|
"""
|
|
get the template of this page if defined or if closer parent if
|
|
defined or DEFAULT_PAGE_TEMPLATE otherwise.
|
|
|
|
If the page has an apphook assigned and its root view exposes a
|
|
template, that template wins — it's what the apphook actually renders
|
|
at the page URL, so the structure board's placeholder layout must
|
|
match it.
|
|
"""
|
|
if hasattr(self, "_template_cache"):
|
|
return self._template_cache
|
|
|
|
if not get_cms_setting("TEMPLATES"):
|
|
return ""
|
|
|
|
if app_hooks and self.page.application_urls:
|
|
from cms.apphook_pool import apphook_pool
|
|
|
|
apphook = apphook_pool.get_apphook(self.page.application_urls)
|
|
if apphook is not None:
|
|
apphook_template = apphook.get_root_template(page=self.page, language=self.language)
|
|
if apphook_template:
|
|
self._template_cache = apphook_template
|
|
return self._template_cache
|
|
|
|
if self.template != constants.TEMPLATE_INHERITANCE_MAGIC:
|
|
self._template_cache = self.template or self.template_choices[0][0]
|
|
return self._template_cache
|
|
|
|
templates = (
|
|
self.get_ancestor_titles()
|
|
.exclude(template=constants.TEMPLATE_INHERITANCE_MAGIC)
|
|
.order_by("-page__path")
|
|
.values_list("template", flat=True)
|
|
)
|
|
|
|
try:
|
|
self._template_cache = templates[0]
|
|
except IndexError:
|
|
self._template_cache = self.template_choices[0][0] if self.template_choices else ""
|
|
return self._template_cache
|
|
|
|
def get_template_name(self):
|
|
"""
|
|
get the textual name (2nd parameter in get_cms_setting('TEMPLATES'))
|
|
of the template of this title. 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 is_editable(self, request):
|
|
"""returns True if page content object itself can be edited. Does not check
|
|
user permissions to do that.
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
return True
|
|
|
|
def is_public(self):
|
|
"""Return whether this content object is visible to the public, i.e. whether
|
|
the default manager returns it. Without a versioning package every saved
|
|
page content is public; with a versioning package installed only published
|
|
versions are. The page's URL is only derived from public content
|
|
(see :meth:`cms.models.pagemodel.Page.update_urls_from_content`).
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
if self.pk is None:
|
|
return False
|
|
return self.__class__.objects.filter(pk=self.pk).exists()
|
|
|
|
def content_indicator(self):
|
|
"""returns the content indicator status. Without additional packages like
|
|
djangocms-versioning page content always is public.
|
|
|
|
:rtype: ``str``
|
|
"""
|
|
return "public" if self.get_absolute_url(language=self.language) else "unpublished-parent"
|
|
|
|
def get_xframe_options(self):
|
|
"""Finds X_FRAME_OPTION from tree if inherited"""
|
|
xframe_options = self.xframe_options or constants.X_FRAME_OPTIONS_INHERIT
|
|
|
|
if xframe_options != constants.X_FRAME_OPTIONS_INHERIT:
|
|
return xframe_options
|
|
|
|
# Ignore those pages which just inherit their value
|
|
ancestors = self.get_ancestor_titles().order_by("-page__path")
|
|
ancestors = ancestors.exclude(xframe_options=constants.X_FRAME_OPTIONS_INHERIT)
|
|
|
|
# Now just give me the clickjacking setting (not anything else)
|
|
xframe_options = ancestors.values_list("xframe_options", flat=True)
|
|
|
|
try:
|
|
return xframe_options[0]
|
|
except IndexError:
|
|
return None
|
|
|
|
def get_absolute_url(self, language=None):
|
|
"""Get the absolute url for the page content. If language is specified it will return
|
|
the absolute url of the corresponding "sister" content."""
|
|
return self.page.get_absolute_url(language=language or self.language)
|
|
|
|
|
|
class EmptyPageContent:
|
|
"""
|
|
Empty title object, can be returned from :meth:`cms.models.pagemodel.Page.get_content_obj` if required
|
|
title object doesn't exist.
|
|
"""
|
|
|
|
title = ""
|
|
slug = ""
|
|
overwrite_url = None
|
|
meta_description = ""
|
|
redirect = ""
|
|
application_urls = ""
|
|
menu_title = ""
|
|
page_title = ""
|
|
xframe_options = None
|
|
template = None
|
|
soft_root = False
|
|
in_navigation = False
|
|
|
|
def __init__(self, language, page=None):
|
|
self.language = language
|
|
self.page = page
|
|
if get_cms_setting("TEMPLATES"):
|
|
self.template = get_cms_setting("TEMPLATES")[0][0]
|
|
else:
|
|
self.template = ""
|
|
|
|
def __bool__(self):
|
|
return False
|
|
|
|
def content_indicator(self):
|
|
"""returns the content indicator status. Empty page content always is empty
|
|
|
|
:rtype: ``str``
|
|
"""
|
|
return "empty"
|
|
|
|
def is_editable(self, request):
|
|
"""returns True if empty page content object itself can be edited. Since editing creates a new
|
|
page content object this should always be True
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
return True
|