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