Source code for mezzanine.pages.models

from __future__ import unicode_literals
from future.builtins import str
from mezzanine.utils.sites import override_current_site_id

try:
    from urllib.parse import urljoin
except ImportError:  # Python 2
    from urlparse import urljoin

from django.urls import resolve, reverse
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _, ugettext

from mezzanine.conf import settings
from mezzanine.core.models import (
    ContentTyped, Displayable, Orderable, RichText)
from mezzanine.pages.fields import MenusField
from mezzanine.pages.managers import PageManager
from mezzanine.utils.urls import path_to_slug
from mezzanine.core.models import wrapped_manager


[docs]class BasePage(Orderable, Displayable): """ Exists solely to store ``PageManager`` as the main manager. If it's defined on ``Page``, a concrete model, then each ``Page`` subclass loses the custom manager. """ objects = wrapped_manager(PageManager) class Meta: abstract = True
[docs]@python_2_unicode_compatible class Page(BasePage, ContentTyped): """ A page in the page tree. This is the base class that custom content types need to subclass. """ parent = models.ForeignKey("Page", on_delete=models.CASCADE, blank=True, null=True, related_name="children") in_menus = MenusField(_("Show in menus"), blank=True, null=True) titles = models.CharField(editable=False, max_length=1000, null=True) login_required = models.BooleanField(_("Login required"), default=False, help_text=_("If checked, only logged in users can view this page")) class Meta: verbose_name = _("Page") verbose_name_plural = _("Pages") ordering = ("titles",) order_with_respect_to = "parent" def __str__(self): return self.titles
[docs] def get_absolute_url(self): """ URL for a page - for ``Link`` page types, simply return its slug since these don't have an actual URL pattern. Also handle the special case of the homepage being a page object. """ slug = self.slug if self.content_model == "link": # Ensure the URL is absolute. slug = urljoin('/', slug) return slug if slug == "/": return reverse("home") else: return reverse("page", kwargs={"slug": slug})
[docs] def save(self, *args, **kwargs): """ Create the titles field using the titles up the parent chain and set the initial value for ordering. """ self.set_content_model() titles = [self.title] parent = self.parent while parent is not None: titles.insert(0, parent.title) parent = parent.parent self.titles = " / ".join(titles) super(Page, self).save(*args, **kwargs)
[docs] def description_from_content(self): """ Override ``Displayable.description_from_content`` to load the content type subclass for when ``save`` is called directly on a ``Page`` instance, so that all fields defined on the subclass are available for generating the description. """ if self.__class__ == Page: if self.content_model: return self.get_content_model().description_from_content() return super(Page, self).description_from_content()
[docs] def get_ascendants(self, for_user=None): """ Returns the ascendants for the page. Ascendants are cached in the ``_ascendants`` attribute, which is populated when the page is loaded via ``Page.objects.with_ascendants_for_slug``. """ if not self.parent_id: # No parents at all, bail out. return [] if not hasattr(self, "_ascendants"): # _ascendants has not been either page.get_ascendants or # Page.objects.assigned by with_ascendants_for_slug, so # run it to see if we can retrieve all parents in a single # query, which will occur if the slugs for each of the pages # have not been customised. if self.slug: kwargs = {"for_user": for_user} with override_current_site_id(self.site_id): pages = Page.objects.with_ascendants_for_slug(self.slug, **kwargs) self._ascendants = pages[0]._ascendants else: self._ascendants = [] if not self._ascendants: # Page has a parent but with_ascendants_for_slug failed to # find them due to custom slugs, so retrieve the parents # recursively. child = self while child.parent_id is not None: self._ascendants.append(child.parent) child = child.parent return self._ascendants
[docs] def get_slug(self): """ Recursively build the slug from the chain of parents. """ slug = super(Page, self).get_slug() if self.parent is not None: return "%s/%s" % (self.parent.slug, slug) return slug
[docs] def set_slug(self, new_slug): """ Changes this page's slug, and all other pages whose slugs start with this page's slug. """ slug_prefix = "%s/" % self.slug for page in Page.objects.filter(slug__startswith=slug_prefix): if not page.overridden(): page.slug = new_slug + page.slug[len(self.slug):] page.save() self.slug = new_slug self.save()
[docs] def set_parent(self, new_parent): """ Change the parent of this page, changing this page's slug to match the new parent if necessary. """ self_slug = self.slug old_parent_slug = self.parent.slug if self.parent else "" new_parent_slug = new_parent.slug if new_parent else "" # Make sure setting the new parent won't cause a cycle. parent = new_parent while parent is not None: if parent.pk == self.pk: raise AttributeError("You can't set a page or its child as" " a parent.") parent = parent.parent self.parent = new_parent self.save() if self_slug and not (self.content_model == "link" and self.slug.startswith("http")): if not old_parent_slug: self.set_slug("/".join((new_parent_slug, self.slug))) elif self.slug.startswith(old_parent_slug): new_slug = self.slug.replace(old_parent_slug, new_parent_slug, 1) self.set_slug(new_slug.strip("/"))
[docs] def overridden(self): """ Returns ``True`` if the page's slug has an explicitly defined urlpattern and is therefore considered to be overridden. """ from mezzanine.pages.views import page page_url = reverse("page", kwargs={"slug": self.slug}) resolved_view = resolve(page_url)[0] return resolved_view != page
[docs] def can_add(self, request): """ Dynamic ``add`` permission for content types to override. """ return self.slug != "/"
[docs] def can_change(self, request): """ Dynamic ``change`` permission for content types to override. """ return True
[docs] def can_delete(self, request): """ Dynamic ``delete`` permission for content types to override. """ return True
[docs] def can_move(self, request, new_parent): """ Dynamic ``move`` permission for content types to override. Controls whether a given page move in the page tree is permitted. When the permission is denied, raises a ``PageMoveException`` with a single argument (message explaining the reason). """ pass
[docs] def set_helpers(self, context): """ Called from the ``page_menu`` template tag and assigns a handful of properties based on the current page, that are used within the various types of menus. """ current_page = context["_current_page"] current_page_id = getattr(current_page, "id", None) current_parent_id = getattr(current_page, "parent_id", None) # Am I a child of the current page? self.is_current_child = self.parent_id == current_page_id self.is_child = self.is_current_child # Backward compatibility # Is my parent the same as the current page's? self.is_current_sibling = self.parent_id == current_parent_id # Am I the current page? try: request = context["request"] except KeyError: # No request context, most likely when tests are run. self.is_current = False else: self.is_current = self.slug == path_to_slug(request.path_info) # Is the current page me or any page up the parent chain? def is_c_or_a(page_id): parent_id = context.get("_parent_page_ids", {}).get(page_id) return self.id == page_id or (parent_id and is_c_or_a(parent_id)) self.is_current_or_ascendant = lambda: bool(is_c_or_a(current_page_id)) self.is_current_parent = self.id == current_parent_id # Am I a primary page? self.is_primary = self.parent_id is None # What's an ID I can use in HTML? self.html_id = self.slug.replace("/", "-") # Default branch level - gets assigned in the page_menu tag. self.branch_level = 0
def in_menu_template(self, template_name): if self.in_menus is not None: for i, l, t in settings.PAGE_MENU_TEMPLATES: if not str(i) in self.in_menus and t == template_name: return False return True
[docs] def get_template_name(self): """ Subclasses can implement this to provide a template to use in ``mezzanine.pages.views.page``. """ return None
[docs]class RichTextPage(Page, RichText): """ Implements the default type of page with a single Rich Text content field. """ class Meta: verbose_name = _("Rich text page") verbose_name_plural = _("Rich text pages")
[docs]class PageMoveException(Exception): """ Raised by ``can_move()`` when the move permission is denied. Takes an optinal single argument: a message explaining the denial. """ def __init__(self, msg=None): self.msg = msg or ugettext("Illegal page move") def __str__(self): return self.msg __unicode__ = __str__