meeb / django-distill

Minimal configuration static site generator for Django
MIT License
441 stars 35 forks source link

Django CMS #34

Closed mateoreyes closed 3 years ago

mateoreyes commented 3 years ago

Hi, I am trying to install django-distil in a project with Django CMS. It's compatible ?

My problem is in the urls.py, in these lines:

urlpatterns = [
        distill_path('',
                 details,
                 name='blog-index',
                 distill_func=get_index,
                 # / is not a valid file name! override it to index.html
                 distill_file='index.html'),
    # e.g. /post/123-some-post-title using named parameters

I get the following error when running distil-local:

CommandError: Failed to render view "/": details() missing 1 required positional argument: 'slug'

if I pass slug = None on details() I get the following error:

TypeError: details() missing 1 required positional argument: 'request'

I don't know much about it, but I think the problem is class-based views and function-based views.

This is the code for the views.py of Django CMS:


from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from django.utils.cache import patch_cache_control
from django.utils.http import is_safe_url, urlquote
from django.utils.timezone import now
from django.utils.translation import get_language_from_request
from django.views.decorators.http import require_POST

from cms.cache.page import get_page_cache
from cms.exceptions import LanguageError
from cms.forms.login import CMSToolbarLoginForm
from cms.models.pagemodel import TreeNode
from cms.page_rendering import _handle_no_page, render_page, render_object_structure, _render_welcome_page
from cms.toolbar.utils import get_toolbar_from_request
from cms.utils import get_current_site
from cms.utils.conf import get_cms_setting
from cms.utils.i18n import (get_fallback_languages, get_public_languages,
                            get_redirect_on_fallback, get_language_list,
                            get_default_language_for_site,
                            is_language_prefix_patterns_used)
from cms.utils.page import get_page_from_request
from cms.utils.page_permissions import user_can_change_page

def _clean_redirect_url(redirect_url, language):
    if (redirect_url and is_language_prefix_patterns_used() and redirect_url[0] == "/"
            and not redirect_url.startswith('/%s/' % language)):
        # add language prefix to url
        redirect_url = "/%s/%s" % (language, redirect_url.lstrip("/"))
    return redirect_url

def details(request, slug):
    """
    The main view of the Django-CMS! Takes a request and a slug, renders the
    page.
    """
    response_timestamp = now()
    if get_cms_setting("PAGE_CACHE") and (
        not hasattr(request, 'toolbar') or (
            not request.toolbar.edit_mode_active and
            not request.toolbar.show_toolbar and
            not request.user.is_authenticated
        )
    ):
        cache_content = get_page_cache(request)
        if cache_content is not None:
            content, headers, expires_datetime = cache_content
            response = HttpResponse(content)
            response.xframe_options_exempt = True
            response._headers = headers
            # Recalculate the max-age header for this cached response
            max_age = int(
                (expires_datetime - response_timestamp).total_seconds() + 0.5)
            patch_cache_control(response, max_age=max_age)
            return response

    # Get a Page model object from the request
    site = get_current_site()
    page = get_page_from_request(request, use_path=slug)
    toolbar = get_toolbar_from_request(request)
    tree_nodes = TreeNode.objects.get_for_site(site)

    if not page and not slug and not tree_nodes.exists():
        # render the welcome page if the requested path is root "/"
        # and there's no pages
        return _render_welcome_page(request)

    if not page:
        # raise 404
        _handle_no_page(request)

    request.current_page = page

    if hasattr(request, 'user') and request.user.is_staff:
        user_languages = get_language_list(site_id=site.pk)
    else:
        user_languages = get_public_languages(site_id=site.pk)

    request_language = get_language_from_request(request, check_path=True)

    if not page.is_home and request_language not in user_languages:
        # The homepage is treated differently because
        # when a request goes to the root of the site (/)
        # without a language, Django will redirect to the user's
        # browser language which might not be a valid cms language,
        # this means we need to correctly redirect that request.
        return _handle_no_page(request)

    # get_published_languages will return all languages in draft mode
    # and published only in live mode.
    # These languages are then filtered out by the user allowed languages
    available_languages = [
        language for language in user_languages
        if language in list(page.get_published_languages())
    ]

    own_urls = [
        request.build_absolute_uri(request.path),
        '/%s' % request.path,
        request.path,
    ]

    try:
        redirect_on_fallback = get_redirect_on_fallback(request_language, site_id=site.pk)
    except LanguageError:
        redirect_on_fallback = False

    if request_language not in user_languages:
        # Language is not allowed
        # Use the default site language
        default_language = get_default_language_for_site(site.pk)
        fallbacks = get_fallback_languages(default_language, site_id=site.pk)
        fallbacks = [default_language] + fallbacks
    else:
        fallbacks = get_fallback_languages(request_language, site_id=site.pk)

    # Only fallback to languages the user is allowed to see
    fallback_languages = [
        language for language in fallbacks
        if language != request_language and language in available_languages
    ]
    language_is_unavailable = request_language not in available_languages

    if language_is_unavailable and not fallback_languages:
        # There is no page with the requested language
        # and there's no configured fallbacks
        return _handle_no_page(request)
    elif language_is_unavailable and (redirect_on_fallback or page.is_home):
        # There is no page with the requested language and
        # the user has explicitly requested to redirect on fallbacks,
        # so redirect to the first configured / available fallback language
        fallback = fallback_languages[0]
        redirect_url = page.get_absolute_url(fallback, fallback=False)
    else:
        page_path = page.get_absolute_url(request_language)
        page_slug = page.get_path(request_language) or page.get_slug(request_language)

        if slug and slug != page_slug and request.path[:len(page_path)] != page_path:
            # The current language does not match its slug.
            # Redirect to the current language.
            return HttpResponseRedirect(page_path)
        # Check if the page has a redirect url defined for this language.
        redirect_url = page.get_redirect(request_language, fallback=False) or ''
        redirect_url = _clean_redirect_url(redirect_url, request_language)

    if redirect_url:
        if request.user.is_staff and toolbar.edit_mode_active:
            toolbar.redirect_url = redirect_url
        elif redirect_url not in own_urls:
            # prevent redirect to self
            return HttpResponseRedirect(redirect_url)

    # permission checks
    if page.login_required and not request.user.is_authenticated:
        return redirect_to_login(urlquote(request.get_full_path()), settings.LOGIN_URL)

    if hasattr(request, 'toolbar'):
        request.toolbar.set_object(page)

    structure_requested = get_cms_setting('CMS_TOOLBAR_URL__BUILD') in request.GET

    if user_can_change_page(request.user, page) and structure_requested:
        return render_object_structure(request, page)
    return render_page(request, page, current_language=request_language, slug=slug)

@require_POST
def login(request):
    redirect_to = request.GET.get(REDIRECT_FIELD_NAME)

    if not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
        redirect_to = reverse("pages-root")

    if request.user.is_authenticated:
        return HttpResponseRedirect(redirect_to)

    form = CMSToolbarLoginForm(request=request, data=request.POST)

    if form.is_valid():
        auth_login(request, form.user_cache)
    else:
        redirect_to += u'?cms_toolbar_login_error=1'
    return HttpResponseRedirect(redirect_to)

I hope you can help me, thank you very much.

meeb commented 3 years ago

Yes, django-distill is specifically designed to create static websites from Django-powered websites so it is compatible. The error you are seeing is because you have defined a URL pattern with no parameters '' and your view requires a parameter slug which is why it's complaining that your view is requiring a parameter you are not passing to it. Change your URL path to '<slug:slug>' (so distill_path('<slug:slug>',... etc.) to pass in a parameter to the view.

Obviously as you have dynamic URLs, I assume like /page1 and /page2 using distill_file= isn't useful as all the pages will be saved with the same index.html filename at the moment and overwrite each other.

Your distill_func method should return and iterable that contains all the page slugs you want to generate. Example:

# in urls.py

def get_pages():
  # Assuming you have a MyPageModel which has a 'slug' field
  for page in MyPageModel.objects.all():
    # The URL of '<slug:slug>' requires one position parameter, return a tuple containing one item
    yield (page.slug,)

urlpatterns = [
  distill_path(
    '<slug:slug>',
    details,
    name='blog-pages',
    distill_func=get_pages
  ),
]
# in views.py

def details(request, slug):
  return render_page(...)

Remember your site should "work" before you apply django-distill - distill does nothing but wraps your existing working URL patterns and then mashes your URLs up with functions that iterate over the URLs you want to generate static versions of, that's about it.

You just have make sure your URL patterns, the iterable that distill_func returns and the parameters that your view requires all match up.

mateoreyes commented 3 years ago

Thanks for your answer !

I did the following:

# urls.py

from cms.models.pagemodel import Page

def get_pages():
  # Assuming you have a MyPageModel which has a 'slug' field
  for page in Page.objects.all():
    # The URL of '<slug:slug>' requires one position parameter, return a tuple containing one item
    yield (page.get_slug(),)

urlpatterns = [
  distill_path(
    '<slug:slug>',
    details,
    name='blog-pages',
    distill_func=get_pages
  ),
]

But I get this error:

CommandError: Failed to render view "/pagina-uno": 'WSGIRequest' object has no attribute 'user'


# pagemodel.py
import copy
from collections import OrderedDict
from logging import getLogger
from os.path import join

from django.contrib.sites.models import Site
from django.urls import reverse
from django.db import models
from django.db.models.base import ModelState
from django.db.models.functions import Concat
from django.utils.encoding import force_text
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import (
    get_language,
    override as force_language,
    gettext_lazy as _,
)

from cms import constants
from cms.constants import PUBLISHER_STATE_DEFAULT, PUBLISHER_STATE_PENDING, PUBLISHER_STATE_DIRTY, TEMPLATE_INHERITANCE_MAGIC
from cms.exceptions import PublicIsUnmodifiable, PublicVersionNeeded, LanguageError
from cms.models.managers import PageManager, PageNodeManager
from cms.utils import i18n
from cms.utils.conf import get_cms_setting
from cms.utils.page import get_clean_username
from cms.utils.i18n import get_current_language

from menus.menu_pool import menu_pool

from treebeard.mp_tree import MP_Node

logger = getLogger(__name__)

class TreeNode(MP_Node):

    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_nodes',
        db_index=True,
    )

    objects = PageNodeManager()

    class Meta:
        app_label = 'cms'
        ordering = ('path',)
        default_permissions = []

    def __str__(self):
        return self.path

    @cached_property
    def item(self):
        return self.get_item()

    def get_item(self):
        # Paving the way...
        return Page.objects.get(node=self, publisher_is_draft=True)

    @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(pos, *args, **kwargs)

    def update(self, **data):
        cls = self.__class__
        cls.objects.filter(pk=self.pk).update(**data)

        for field, value in data.items():
            setattr(self, field, value)
        return

    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 _reload(self):
        """
        Reload a page node from the database
        """
        return self.__class__.objects.get(pk=self.pk)

    def _has_cached_hierarchy(self):
        return hasattr(self, '_descendants') and hasattr(self, '_ancestors')

    def _set_hierarchy(self, nodes, ancestors=None):
        if self.is_branch:
            self._descendants = [node for node in nodes
                           if node.path.startswith(self.path)
                           and node.depth > self.depth]
        else:
            self._descendants = []

        if self.is_root():
            self._ancestors = []
        else:
            self._ancestors = ancestors

        children = (node for node in self._descendants
                    if node.depth == self.depth + 1)

        for child in children:
            child._set_hierarchy(self._descendants, ancestors=([self] + self._ancestors))

class Page(models.Model):
    """
    A simple hierarchical page model
    """
    LIMIT_VISIBILITY_IN_MENU_CHOICES = (
        (constants.VISIBILITY_USERS, _('for logged in users only')),
        (constants.VISIBILITY_ANONYMOUS, _('for anonymous users only')),
    )
    TEMPLATE_DEFAULT = TEMPLATE_INHERITANCE_MAGIC if get_cms_setting('TEMPLATE_INHERITANCE') else get_cms_setting('TEMPLATES')[0][0]

    X_FRAME_OPTIONS_INHERIT = constants.X_FRAME_OPTIONS_INHERIT
    X_FRAME_OPTIONS_DENY = constants.X_FRAME_OPTIONS_DENY
    X_FRAME_OPTIONS_SAMEORIGIN = constants.X_FRAME_OPTIONS_SAMEORIGIN
    X_FRAME_OPTIONS_ALLOW = constants.X_FRAME_OPTIONS_ALLOW
    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'))
    )

    template_choices = [(x, _(y)) for x, y in get_cms_setting('TEMPLATES')]

    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)

    publication_date = models.DateTimeField(_("publication date"), null=True, blank=True, help_text=_(
        'When the page should go live. Status must be "Published" for page to go live.'), db_index=True)
    publication_end_date = models.DateTimeField(_("publication end date"), null=True, blank=True,
                                                help_text=_('When to expire the page. Leave empty to never expire.'),
                                                db_index=True)
    #
    # Please use toggle_in_navigation() instead of affecting this property
    # directly so that the cms page cache can be invalidated as appropriate.
    #
    in_navigation = models.BooleanField(_("in navigation"), default=True, db_index=True)
    soft_root = models.BooleanField(_("soft root"), db_index=True, default=False,
                                    help_text=_("All ancestors will not be displayed in the navigation"))
    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)
    template = models.CharField(_("template"), max_length=100, choices=template_choices,
                                help_text=_('The template used to render the content.'),
                                default=TEMPLATE_DEFAULT)

    login_required = models.BooleanField(_("login required"), default=False)
    limit_visibility_in_menu = models.SmallIntegerField(_("menu visibility"), default=None, null=True, blank=True,
                                                        choices=LIMIT_VISIBILITY_IN_MENU_CHOICES, db_index=True,
                                                        help_text=_("limit when this page is visible in the menu"))
    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)

    # Placeholders (plugins)
    placeholders = models.ManyToManyField('cms.Placeholder', editable=False)

    # Publisher fields
    publisher_is_draft = models.BooleanField(default=True, editable=False, db_index=True)
    # This is misnamed - the one-to-one relation is populated on both ends
    publisher_public = models.OneToOneField(
        'self',
        on_delete=models.CASCADE,
        related_name='publisher_draft',
        null=True,
        editable=False,
    )
    languages = models.CharField(max_length=255, editable=False, blank=True, null=True)

    # X Frame Options for clickjacking protection
    xframe_options = models.IntegerField(
        choices=X_FRAME_OPTIONS_CHOICES,
        default=get_cms_setting('DEFAULT_X_FRAME_OPTIONS'),
    )

    # Flag that marks a page as page-type
    is_page_type = models.BooleanField(default=False)

    node = models.ForeignKey(
        TreeNode,
        related_name='cms_pages',
        on_delete=models.CASCADE,
    )

    # 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'),
        )
        unique_together = ('node', 'publisher_is_draft')
        verbose_name = _('page')
        verbose_name_plural = _('pages')
        app_label = 'cms'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.title_cache = {}

    def __str__(self):
        try:
            title = self.get_menu_title(fallback=True)
        except LanguageError:
            try:
                title = self.title_set.all()[0]
            except IndexError:
                title = None
        if title is None:
            title = u""
        return force_text(title)

    def __repr__(self):
        display = '<{module}.{class_name} id={id} is_draft={is_draft} object at {location}>'.format(
            module=self.__module__,
            class_name=self.__class__.__name__,
            id=self.pk,
            is_draft=self.publisher_is_draft,
            location=hex(id(self)),
        )
        return display

    def _clear_node_cache(self):
        if Page.node.is_cached(self):
            Page.node.field.delete_cached_value(self)

    def _clear_internal_cache(self):
        self.title_cache = {}
        self._clear_node_cache()

        if hasattr(self, '_prefetched_objects_cache'):
            del self._prefetched_objects_cache

    @cached_property
    def parent_page(self):
        return self.get_parent_page()

    def set_as_homepage(self, user=None):
        """
        Sets the given page as the homepage.
        Updates the title paths for all affected pages.
        Returns the old home page (if any).
        """
        assert self.publisher_is_draft

        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,
                node__site=self.node.site_id,
                publisher_is_draft=True,
            )
        except self.__class__.DoesNotExist:
            old_home_tree = []
        else:
            old_home.update(
                draft_only=False,
                is_home=False,
                changed_by=changed_by,
                changed_date=changed_date,
            )
            old_home_tree = old_home._set_title_root_path()

        self.update(
            draft_only=False,
            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 _update_title_path(self, language):
        parent_page = self.get_parent_page()

        if parent_page:
            base = parent_page.get_path(language, fallback=True)
        else:
            base = ''

        title_obj = self.get_title_obj(language, fallback=False)
        title_obj.path = title_obj.get_path_for_base(base)
        title_obj.save()

    def _update_title_path_recursive(self, language):
        assert self.publisher_is_draft
        from cms.models import Title

        if self.node.is_leaf() or language not in self.get_languages():
            return

        pages = self.get_child_pages()
        base = self.get_path(language, fallback=True)

        if base:
            new_path = Concat(models.Value(base), models.Value('/'), models.F('slug'))
        else:
            # User is moving the homepage
            new_path = models.F('slug')

        (Title
         .objects
         .filter(language=language, page__in=pages)
         .exclude(has_url_overwrite=True)
         .update(path=new_path))

        for child in pages.filter(title_set__language=language).iterator():
            child._update_title_path_recursive(language)

    def _set_title_root_path(self):
        from cms.models import Title

        node_tree = TreeNode.get_tree(self.node)
        page_tree = self.__class__.objects.filter(node__in=node_tree)
        translations = Title.objects.filter(page__in=page_tree, has_url_overwrite=False)

        for language, slug in self.title_set.values_list('language', 'slug'):
            # Update the translations for all descendants of this page
            # to include this page's slug as its path prefix
            (translations
             .filter(language=language)
             .update(path=Concat(models.Value(slug), models.Value('/'), 'path')))

            # Explicitly update this page's path to match its slug
            # Doing this is cheaper than a TRIM call to remove the "/" characters
            if self.publisher_public_id:
                # include the public translation
                current_translations = Title.objects.filter(page__in=[self.pk, self.publisher_public_id])
            else:
                current_translations = self.title_set.all()
            current_translations.filter(language=language).update(path=slug)
        return page_tree

    def _remove_title_root_path(self):
        from cms.models import Title

        node_tree = TreeNode.get_tree(self.node)
        page_tree = self.__class__.objects.filter(node__in=node_tree)
        translations = Title.objects.filter(page__in=page_tree, has_url_overwrite=False)

        for language, slug in self.title_set.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',
            )
            (translations
             .filter(language=language, path__startswith=slug)
             .update(path=sql_func))
        return page_tree

    def is_dirty(self, language):
        state = self.get_publisher_state(language)
        return state == PUBLISHER_STATE_DIRTY or state == PUBLISHER_STATE_PENDING

    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
        """
        assert self.publisher_is_draft
        # Only root nodes are eligible for homepage
        return not self.is_home and bool(self.node.is_root())

    def get_absolute_url(self, language=None, fallback=True):
        if not language:
            language = get_current_language()

        with force_language(language):
            if self.is_home:
                return reverse('pages-root')
            path = self.get_path(language, fallback) or self.get_slug(language, fallback)
            return reverse('pages-details-by-slug', kwargs={"slug": path})

    def get_public_url(self, language=None, fallback=True):
        """
        Returns the URL of the published version of the current page.
        Returns empty string if the page is not published.
        """
        try:
            return self.get_public_object().get_absolute_url(language, fallback)
        except:
            return ''

    def get_draft_url(self, language=None, fallback=True):
        """
        Returns the URL of the draft version of the current page.
        Returns empty string if the draft page is not available.
        """
        try:
            return self.get_draft_object().get_absolute_url(language, fallback)
        except:
            return ''

    def set_tree_node(self, site, target=None, position='first-child'):
        assert self.publisher_is_draft
        assert position in ('last-child', 'first-child', 'left', 'right')

        new_node = TreeNode(site=site)

        if target is None:
            self.node = TreeNode.add_root(instance=new_node)
        elif position == 'first-child' and target.is_branch:
            self.node = target.get_first_child().add_sibling(pos='left', instance=new_node)
        elif position in ('last-child', 'first-child'):
            self.node = target.add_child(instance=new_node)
        else:
            self.node = target.add_sibling(pos=position, instance=new_node)

    def move_page(self, target_node, position='first-child'):
        """
        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, but after move is done page_moved signal is fired.

        Note for issue #1166: url conflicts are handled by updated
        check_title_slugs, overwrite_url on the moved page don't need any check
        as it remains the same regardless of the page position in the tree
        """
        assert self.publisher_is_draft
        assert isinstance(target_node, TreeNode)

        inherited_template = self.template == constants.TEMPLATE_INHERITANCE_MAGIC

        if inherited_template and target_node.is_root() and position in ('left', 'right'):
            # The page is being moved to a root position.
            # Explicitly set the inherited template on the page
            # to keep all plugins / placeholders.
            self.update(refresh=False, template=self.get_template())

        # Don't use a cached node. Always get a fresh one.
        self._clear_node_cache()

        # Runs the SQL updates on the treebeard fields
        self.node.move(target_node, position)

        if position in ('first-child', 'last-child'):
            parent_id = target_node.pk
        else:
            # moving relative to sibling
            # or to the root of the tree
            parent_id = target_node.parent_id
        # Runs the SQL updates on the parent field
        self.node.update(parent_id=parent_id)

        # Clear the cached node once again to trigger a db query
        # on access.
        self._clear_node_cache()

        # Update the descendants to "PENDING"
        # If the target (parent) page is not published
        # and the page being moved is published.
        titles = (
            self
            .title_set
            .filter(language__in=self.get_languages())
            .values_list('language', 'published')
        )
        parent_page = self.get_parent_page()

        if parent_page:
            parent_titles = (
                parent_page
                .title_set
                .exclude(publisher_state=PUBLISHER_STATE_PENDING)
                .values_list('language', 'published')
            )
            parent_titles_by_language = dict(parent_titles)
        else:
            parent_titles_by_language = {}

        for language, published in titles:
            parent_is_published = parent_titles_by_language.get(language)

            # Update draft title path
            self._update_title_path(language)
            self._update_title_path_recursive(language)

            if published and parent_is_published:
                # this looks redundant but it's necessary
                # for all the descendants of the page being
                # moved to be set to the correct state.
                self.publisher_public._update_title_path(language)
                self.mark_as_published(language)
                self.mark_descendants_as_published(language)
            elif published and parent_page:
                # page is published but it's parent is not
                # mark the page being moved (source) as "pending"
                self.mark_as_pending(language)
                # mark all descendants of source as "pending"
                self.mark_descendants_pending(language)
            elif published:
                self.publisher_public._update_title_path(language)
                self.mark_as_published(language)
                self.mark_descendants_as_published(language)
        self.clear_cache(menu=True)
        return self

    def _copy_titles(self, target, language, published):
        """
        Copy the title matching language to a new page (which must have a pk).
        :param target: The page where the new title should be stored
        """
        source_title = self.title_set.get(language=language)

        try:
            target_title_id = (
                target
                .title_set
                .filter(language=language)
                .values_list('pk', flat=True)[0]
            )
        except IndexError:
            target_title_id = None

        source_title_id = source_title.pk

        # If an old title exists, overwrite. Otherwise create new
        source_title.pk = target_title_id
        source_title.page = target
        source_title.publisher_is_draft = target.publisher_is_draft
        source_title.publisher_public_id = source_title_id
        source_title.published = published
        source_title._publisher_keep_state = True

        if published:
            source_title.publisher_state = PUBLISHER_STATE_DEFAULT
        else:
            source_title.publisher_state = PUBLISHER_STATE_PENDING
        source_title.save()
        return source_title

    def _clear_placeholders(self, language=None):
        from cms.models import CMSPlugin

        placeholders = list(self.get_placeholders())
        placeholder_ids = (placeholder.pk for placeholder in placeholders)
        plugins = CMSPlugin.objects.filter(placeholder__in=placeholder_ids)

        if language:
            plugins = plugins.filter(language=language)
        models.query.QuerySet.delete(plugins)
        return placeholders

    def _copy_contents(self, target, language):
        """
        Copy all the plugins to a new page.
        :param target: The page where the new content should be stored
        """
        cleared_placeholders = target._clear_placeholders(language)
        cleared_placeholders_by_slot = {pl.slot: pl for pl in cleared_placeholders}

        for placeholder in self.get_placeholders():
            try:
                target_placeholder = cleared_placeholders_by_slot[placeholder.slot]
            except KeyError:
                target_placeholder = target.placeholders.create(
                    slot=placeholder.slot,
                    default_width=placeholder.default_width,
                )

            placeholder.copy_plugins(target_placeholder, language=language)

    def _copy_attributes(self, target, clean=False):
        """
        Copy all page data to the target. This excludes parent and other values
        that are specific to an exact instance.
        :param target: The Page to copy the attributes to
        """
        if not clean:
            target.publication_date = self.publication_date
            target.publication_end_date = self.publication_end_date
            target.reverse_id = self.reverse_id
        target.changed_by = self.changed_by
        target.login_required = self.login_required
        target.in_navigation = self.in_navigation
        target.soft_root = self.soft_root
        target.limit_visibility_in_menu = self.limit_visibility_in_menu
        target.navigation_extenders = self.navigation_extenders
        target.application_urls = self.application_urls
        target.application_namespace = self.application_namespace
        target.template = self.template
        target.xframe_options = self.xframe_options
        target.is_page_type = self.is_page_type

    def copy(self, site, parent_node=None, language=None,
             translations=True, permissions=False, extensions=True):
        from cms.utils.page import get_available_slug

        if parent_node:
            new_node = parent_node.add_child(site=site)
            parent_page = parent_node.item
        else:
            new_node = TreeNode.add_root(site=site)
            parent_page = None

        new_page = copy.copy(self)
        new_page._state = ModelState()
        new_page._clear_internal_cache()
        new_page.pk = None
        new_page.node = new_node
        new_page.publisher_public_id = None
        new_page.is_home = False
        new_page.reverse_id = None
        new_page.publication_date = None
        new_page.publication_end_date = None
        new_page.languages = ''
        new_page.save()

        # Have the node remember its page.
        # This is done to save some queries
        # when the node's descendants are copied.
        new_page.node.__dict__['item'] = new_page

        if language and translations:
            translations = self.title_set.filter(language=language)
        elif translations:
            translations = self.title_set.all()
        else:
            translations = self.title_set.none()

        # copy titles of this page
        for title in translations:
            title = copy.copy(title)
            title.pk = None
            title.page = new_page
            title.published = False
            title.publisher_public = None

            if parent_page:
                base = parent_page.get_path(title.language)
                path = '%s/%s' % (base, title.slug) if base else title.slug
            else:
                base = ''
                path = title.slug

            title.slug = get_available_slug(site, path, title.language)
            title.path = '%s/%s' % (base, title.slug) if base else title.slug
            title.save()

            new_page.title_cache[title.language] = title
        new_page.update_languages([trans.language for trans in translations])

        # copy the placeholders (and plugins on those placeholders!)
        for placeholder in self.placeholders.iterator():
            new_placeholder = copy.copy(placeholder)
            new_placeholder.pk = None
            new_placeholder.save()
            new_page.placeholders.add(new_placeholder)
            placeholder.copy_plugins(new_placeholder, language=language)

        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:
                new_page.pagepermission_set.bulk_create(permissions_new)
        return new_page

    def copy_with_descendants(self, target_node=None, position=None,
                              copy_permissions=True, target_site=None):
        """
        Copy a page [ and all its descendants to a new location ]
        """
        if not self.publisher_is_draft:
            raise PublicIsUnmodifiable("copy page is not allowed for public pages")

        if position in ('first-child', 'last-child'):
            parent_node = target_node
        elif target_node:
            parent_node = target_node.parent
        else:
            parent_node = None

        if target_site is None:
            target_site = parent_node.site if parent_node else self.node.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()
            .select_related('node')
            .prefetch_related('title_set')
        )
        new_root_page = self.copy(target_site, parent_node=parent_node)
        new_root_node = new_root_page.node

        if target_node and position in ('first-child'):
            # target node is a parent and user has requested to
            # insert the new page as its first child
            new_root_node.move(target_node, position)
            new_root_node.refresh_from_db(fields=('path', 'depth'))

        if target_node and position in ('left', 'last-child'):
            # target node is a sibling
            new_root_node.move(target_node, position)
            new_root_node.refresh_from_db(fields=('path', 'depth'))

        nodes_by_id = {self.node.pk: new_root_node}

        for page in descendants:
            parent = nodes_by_id[page.node.parent_id]
            new_page = page.copy(
                target_site,
                parent_node=parent,
                translations=True,
                permissions=copy_permissions,
            )
            nodes_by_id[page.node_id] = new_page.node
        return new_root_page

    def delete(self, *args, **kwargs):
        TreeNode.get_tree(self.node).delete_fast()

        if self.node.parent_id:
            (TreeNode
             .objects
             .filter(pk=self.node.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.title_set.filter(language__in=languages).delete()

        for language in languages:
            self.mark_descendants_pending(language)

    def save(self, **kwargs):
        # delete template cache
        if hasattr(self, '_template_cache'):
            delattr(self, '_template_cache')

        created = not bool(self.pk)
        if self.reverse_id == "":
            self.reverse_id = None
        if self.application_namespace == "":
            self.application_namespace = None
        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 save_base(self, *args, **kwargs):
        """Overridden save_base. If an instance is draft, and was changed, mark
        it as dirty.

        Dirty flag is used for changed nodes identification when publish method
        takes place. After current changes are published, state is set back to
        PUBLISHER_STATE_DEFAULT (in publish method).
        """
        keep_state = getattr(self, '_publisher_keep_state', None)
        if self.publisher_is_draft and not keep_state and self.is_new_dirty():
            self.title_set.all().update(publisher_state=PUBLISHER_STATE_DIRTY)
        if keep_state:
            delattr(self, '_publisher_keep_state')
        return super().save_base(*args, **kwargs)

    def update(self, refresh=False, draft_only=True, **data):
        assert self.publisher_is_draft

        cls = self.__class__

        if not draft_only and self.publisher_public_id:
            ids = [self.pk, self.publisher_public_id]
            cls.objects.filter(pk__in=ids).update(**data)
        else:
            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.title_set.filter(language=language)
        else:
            translations = self.title_set.all()
        return translations.update(**data)

    def has_translation(self, language):
        return self.title_set.filter(language=language).exists()

    def is_new_dirty(self):
        if self.pk:
            fields = [
                'publication_date', 'publication_end_date', 'in_navigation', 'soft_root', 'reverse_id',
                'navigation_extenders', 'template', 'login_required', 'limit_visibility_in_menu'
            ]
            try:
                old_page = Page.objects.get(pk=self.pk)
            except Page.DoesNotExist:
                return True
            for field in fields:
                old_val = getattr(old_page, field)
                new_val = getattr(self, field)
                if not old_val == new_val:
                    return True
            return False
        return True

    def is_published(self, language, force_reload=False):
        title_obj = self.get_title_obj(language, fallback=False, force_reload=force_reload)
        return title_obj.published and title_obj.publisher_state != PUBLISHER_STATE_PENDING

    def toggle_in_navigation(self, set_to=None):
        '''
        Toggles (or sets) in_navigation and invalidates the cms page cache
        '''
        old = self.in_navigation
        if set_to in [True, False]:
            self.in_navigation = set_to
        else:
            self.in_navigation = not self.in_navigation
        self.save()

        # If there was a change, invalidate the cms page cache
        if self.in_navigation != old:
            self.clear_cache()
        return self.in_navigation

    def get_publisher_state(self, language, force_reload=False):
        try:
            return self.get_title_obj(language, False, force_reload=force_reload).publisher_state
        except AttributeError:
            return None

    def set_publisher_state(self, language, state, published=None):
        title = self.title_set.get(language=language)
        title.publisher_state = state
        if published is not None:
            title.published = published
        title._publisher_keep_state = True
        title.save()
        if language in self.title_cache:
            self.title_cache[language].publisher_state = state
        return title

    def publish(self, language):
        """
        :returns: True if page was successfully published.
        """
        from cms.utils.permissions import get_current_user_name

        # Publish can only be called on draft pages
        if not self.publisher_is_draft:
            raise PublicIsUnmodifiable('The public instance cannot be published. Use draft.')

        if not self._publisher_can_publish(language):
            return False

        if self.publisher_public_id:
            public_page = Page.objects.get(pk=self.publisher_public_id)
            public_languages = public_page.get_languages()
        else:
            public_page = Page(created_by=self.created_by)
            public_languages = [language]

        self._copy_attributes(public_page, clean=False)

        if language not in public_languages:
            public_languages.append(language)

        # TODO: Get rid of the current user thread hack
        public_page.changed_by = get_current_user_name()
        public_page.is_home = self.is_home
        public_page.publication_date = self.publication_date or now()
        public_page.publisher_public = self
        public_page.publisher_is_draft = False
        public_page.languages = ','.join(public_languages)
        public_page.node = self.node
        public_page.save()

        # Copy the page translation (title) matching language
        # into a "public" version.
        public_title = self._copy_titles(public_page, language, published=True)

        # Ensure this draft page points to its public version
        self.update(
            draft_only=True,
            changed_by=public_page.changed_by,
            publisher_public=public_page,
            publication_date=public_page.publication_date,
        )

        # Set the draft page translation matching language
        # to point to its public version.
        # Its important for draft to be published even if its state
        # is pending.
        self.update_translations(
            language,
            published=True,
            publisher_public=public_title,
            publisher_state=PUBLISHER_STATE_DEFAULT,
        )
        self._copy_contents(public_page, language)

        if self.node.is_branch:
            self.mark_descendants_as_published(language)

        if language in self.title_cache:
            del self.title_cache[language]

        # fire signal after publishing is done
        import cms.signals as cms_signals

        cms_signals.post_publish.send(sender=Page, instance=self, language=language)

        public_page.clear_cache(
            language,
            menu=True,
            placeholder=True,
        )
        return True

    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()

            for placeholder in placeholders:
                placeholder.clear_cache(language, site_id=self.node.site_id)

        if menu:
            # Clears all menu caches for this page's site
            menu_pool.clear(site_id=self.node.site_id)

    def unpublish(self, language, site=None):
        """
        Removes this page from the public site
        :returns: True if this page was successfully unpublished
        """
        # Publish can only be called on draft pages
        if not self.publisher_is_draft:
            raise PublicIsUnmodifiable('The public instance cannot be unpublished. Use draft.')

        self.update_translations(
            language,
            published=False,
            publisher_state=PUBLISHER_STATE_DIRTY,
        )

        public_page = self.get_public_object()
        public_page.update_translations(language, published=False)
        public_page._clear_placeholders(language)
        public_page.clear_cache(language)

        self.mark_descendants_pending(language)

        from cms.signals import post_unpublish
        post_unpublish.send(sender=Page, instance=self, language=language)

        return True

    def get_child_pages(self):
        nodes = self.node.get_children()
        pages = (
            self
            .__class__
            .objects
            .filter(
                node__in=nodes,
                publisher_is_draft=self.publisher_is_draft,
            )
            .order_by('node__path')
        )
        return pages

    def get_ancestor_pages(self):
        nodes = self.node.get_ancestors()
        pages = (
            self
            .__class__
            .objects
            .filter(
                node__in=nodes,
                publisher_is_draft=self.publisher_is_draft,
            )
            .order_by('node__path')
        )
        return pages

    def get_descendant_pages(self):
        nodes = self.node.get_descendants()
        pages = (
            self
            .__class__
            .objects
            .filter(
                node__in=nodes,
                publisher_is_draft=self.publisher_is_draft,
            )
            .order_by('node__path')
        )
        return pages

    def get_root(self):
        node = self.node
        return self.__class__.objects.get(
            node__path=node.path[0:node.steplen],
            publisher_is_draft=self.publisher_is_draft,
        )

    def get_parent_page(self):
        if not self.node.parent_id:
            return None

        pages = Page.objects.filter(
            node=self.node.parent_id,
            publisher_is_draft=self.publisher_is_draft,
        )
        return pages.select_related('node').first()

    def mark_as_pending(self, language):
        assert self.publisher_is_draft
        assert self.publisher_public_id

        self.get_public_object().title_set.filter(language=language).update(published=False)

        if self.get_publisher_state(language) == PUBLISHER_STATE_DEFAULT:
            # Only change the state if the draft page is published
            # and it's state is the default (0), to avoid overriding a dirty state.
            self.set_publisher_state(language, state=PUBLISHER_STATE_PENDING)

    def mark_descendants_pending(self, language):
        from cms.models import Title

        if not self.publisher_is_draft:
            raise PublicIsUnmodifiable('The public instance cannot be altered. Use draft.')

        node_descendants = self.node.get_descendants()
        page_descendants = self.__class__.objects.filter(node__in=node_descendants)

        if page_descendants.filter(publisher_is_draft=True).exists():
            # Only change the state if the draft page is not dirty
            # to avoid overriding a dirty state.
            Title.objects.filter(
                published=True,
                language=language,
                page__in=page_descendants.filter(publisher_is_draft=True),
                publisher_state=PUBLISHER_STATE_DEFAULT,
            ).update(publisher_state=PUBLISHER_STATE_PENDING)

        if page_descendants.filter(publisher_is_draft=False).exists():
            Title.objects.filter(
                published=True,
                language=language,
                page__in=page_descendants.filter(publisher_is_draft=False),
            ).update(published=False)

    def mark_as_published(self, language):
        from cms.models import Title

        (Title
         .objects
         .filter(page=self.publisher_public_id, language=language)
         .update(publisher_state=PUBLISHER_STATE_DEFAULT, published=True))

        draft = self.get_draft_object()

        if draft.get_publisher_state(language) == PUBLISHER_STATE_PENDING:
            # A check for pending is necessary because the page might have
            # been modified after it was marked as pending.
            draft.set_publisher_state(language, PUBLISHER_STATE_DEFAULT)

    def mark_descendants_as_published(self, language):
        from cms.models import Title

        if not self.publisher_is_draft:
            raise PublicIsUnmodifiable('The public instance cannot be published. Use draft.')

        base = self.get_path(language, fallback=True)
        node_children = self.node.get_children()
        page_children = self.__class__.objects.filter(node__in=node_children)
        page_children_draft = page_children.filter(publisher_is_draft=True)
        page_children_public = page_children.filter(publisher_is_draft=False)

        # Set public pending titles as published
        unpublished_public = Title.objects.filter(
            language=language,
            page__in=page_children_public,
            publisher_public__published=True,
        )

        if base:
            new_path = Concat(models.Value(base), models.Value('/'), models.F('slug'))
        else:
            # User is moving the homepage
            new_path = models.F('slug')

        # Update public title paths
        unpublished_public.exclude(has_url_overwrite=True).update(path=new_path)

        # Set unpublished pending titles to published
        unpublished_public.filter(published=False).update(published=True)

        # Update drafts
        Title.objects.filter(
            published=True,
            language=language,
            page__in=page_children_draft,
            publisher_state=PUBLISHER_STATE_PENDING
        ).update(publisher_state=PUBLISHER_STATE_DEFAULT)

        # Continue publishing descendants, one branch at a time.
        published_children = page_children_draft.filter(
            title_set__published=True,
            title_set__language=language,
        )

        for child in published_children.iterator():
            child.mark_descendants_as_published(language)

    def revert_to_live(self, language):
        """Revert the draft version to the same state as the public version
        """
        if not self.publisher_is_draft:
            # Revert can only be called on draft pages
            raise PublicIsUnmodifiable('The public instance cannot be reverted. Use draft.')

        public = self.get_public_object()

        if not public:
            raise PublicVersionNeeded('A public version of this page is needed')

        public._copy_attributes(self)
        public._copy_contents(self, language)
        public._copy_titles(self, language, public.is_published(language))

        self.update_translations(
            language,
            published=True,
            publisher_state=PUBLISHER_STATE_DEFAULT,
        )
        self._publisher_keep_state = True
        self.save()

    def get_draft_object(self):
        if not self.publisher_is_draft:
            return self.publisher_draft
        return self

    def get_public_object(self):
        if not self.publisher_is_draft:
            return self
        return self.publisher_public

    def get_languages(self):
        if self.languages:
            return sorted(self.languages.split(','))
        else:
            return []

    def remove_language(self, language):
        page_languages = self.get_languages()

        if language in page_languages:
            page_languages.remove(language)
            self.update_languages(page_languages)

    def update_languages(self, languages):
        languages = ",".join(languages)
        # Update current instance
        self.languages = languages
        # Commit. It's important to not call save()
        # we'd like to commit only the languages field and without
        # any kind of signals.
        self.update(draft_only=False, languages=languages)

    def get_published_languages(self):
        if self.publisher_is_draft:
            return self.get_languages()
        return sorted([language for language in self.get_languages() if self.is_published(language)])

    def set_translations_cache(self):
        for translation in self.title_set.all():
            self.title_cache.setdefault(translation.language, translation)

    def get_path_for_slug(self, slug, language):
        if self.is_home:
            return ''

        if self.parent_page:
            base = self.parent_page.get_path(language, fallback=True)
            # base can be empty when the parent is a home-page
            path = u'%s/%s' % (base, slug) if base else slug
        else:
            path = slug
        return path

    # ## Title object access

    def get_title_obj(self, language=None, fallback=True, force_reload=False):
        """Helper function for accessing wanted / current title.
        If wanted title doesn't exists, EmptyTitle instance will be returned.
        """
        language = self._get_title_cache(language, fallback, force_reload)
        if language in self.title_cache:
            return self.title_cache[language]
        from cms.models.titlemodels import EmptyTitle

        return EmptyTitle(language)

    def get_title_obj_attribute(self, attrname, language=None, fallback=True, force_reload=False):
        """Helper function for getting attribute or None from wanted/current title.
        """
        try:
            attribute = getattr(self.get_title_obj(language, fallback, force_reload), attrname)
            return attribute
        except AttributeError:
            return None

    def get_path(self, language=None, fallback=True, force_reload=False):
        """
        get the path of the page depending on the given language
        """
        return self.get_title_obj_attribute("path", language, fallback, force_reload)

    def get_slug(self, language=None, fallback=True, force_reload=False):
        """
        get the slug of the page depending on the given language
        """
        return self.get_title_obj_attribute("slug", language, fallback, force_reload)

    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_title_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_title_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):
        if not hasattr(self, '_placeholder_cache'):
            self._placeholder_cache = self.placeholders.all()
        return self._placeholder_cache

    def _validate_title(self, title):
        from cms.models.titlemodels import EmptyTitle
        if isinstance(title, EmptyTitle):
            return False
        if not title.title or not title.slug:
            return False
        return True

    def get_admin_tree_title(self):
        from cms.models.titlemodels import EmptyTitle
        language = get_language()

        if not self.title_cache:
            self.set_translations_cache()

        if language not in self.title_cache or not self._validate_title(self.title_cache.get(language, EmptyTitle(language))):
            fallback_langs = i18n.get_fallback_languages(language)
            found = False
            for lang in fallback_langs:
                if lang in self.title_cache and self._validate_title(self.title_cache.get(lang, EmptyTitle(lang))):
                    found = True
                    language = lang
            if not found:
                language = None
                for lang, item in self.title_cache.items():
                    if not isinstance(item, EmptyTitle):
                        language = lang
        if not language:
            return _("Empty")
        title = self.title_cache[language]
        if title.title:
            return title.title
        if title.page_title:
            return title.page_title
        if title.menu_title:
            return title.menu_title
        return title.slug

    def get_changed_date(self, language=None, fallback=True, force_reload=False):
        """
        get when this page was last updated
        """
        return self.changed_date

    def get_changed_by(self, language=None, fallback=True, force_reload=False):
        """
        get user who last changed this page
        """
        return self.changed_by

    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_title_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_title_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_title_obj_attribute("redirect", language, fallback, force_reload)

    def _get_title_cache(self, language, fallback, force_reload):
        if not language:
            language = get_language()

        force_reload = (force_reload or language not in self.title_cache)

        if fallback and not self.title_cache.get(language):
            # language can be in the cache but might be an EmptyTitle instance
            fallback_langs = i18n.get_fallback_languages(language)
            for lang in fallback_langs:
                if self.title_cache.get(lang):
                    return lang

        if force_reload:
            from cms.models.titlemodels import Title

            titles = Title.objects.filter(page=self)
            for title in titles:
                self.title_cache[title.language] = title
            if self.title_cache.get(language):
                return language
            else:
                if fallback:
                    fallback_langs = i18n.get_fallback_languages(language)
                    for lang in fallback_langs:
                        if self.title_cache.get(lang):
                            return lang
        return language

    def get_template(self):
        """
        get the template of this page if defined or if closer parent if
        defined or DEFAULT_PAGE_TEMPLATE otherwise
        """
        if hasattr(self, '_template_cache'):
            return self._template_cache

        if self.template != constants.TEMPLATE_INHERITANCE_MAGIC:
            self._template_cache = self.template or get_cms_setting('TEMPLATES')[0][0]
            return self._template_cache

        templates = (
            self
            .get_ancestor_pages()
            .exclude(template=constants.TEMPLATE_INHERITANCE_MAGIC)
            .order_by('-node__path')
            .values_list('template', flat=True)
        )

        try:
            self._template_cache = templates[0]
        except IndexError:
            self._template_cache = get_cms_setting('TEMPLATES')[0][0]
        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 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'):
            page = self.get_draft_object()
            restrictions = (
                PagePermission
                .objects
                .for_page(page)
                .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 has_placeholder_change_permission(self, user):
        if not self.publisher_is_draft:
            return False
        return self.has_change_permission(user)

    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 _publisher_can_publish(self, language):
        """Is parent of this object already published?
        """
        if self.is_page_type:
            return False

        if not self.parent_page:
            return True

        if self.parent_page.publisher_public_id:
            return self.parent_page.get_public_object().is_published(language)
        return False

    def rescan_placeholders(self):
        """
        Rescan and if necessary create placeholders in the current template.
        """
        existing = OrderedDict()
        placeholders = [pl.slot for pl in self.get_declared_placeholders()]

        for placeholder in self.placeholders.all():
            if placeholder.slot in placeholders:
                existing[placeholder.slot] = placeholder

        for placeholder in placeholders:
            if placeholder not in existing:
                existing[placeholder] = self.placeholders.create(slot=placeholder)
        return existing

    def get_declared_placeholders(self):
        # inline import to prevent circular imports
        from cms.utils.placeholder import get_placeholders

        return get_placeholders(self.get_template())

    def get_declared_static_placeholders(self, context):
        # inline import to prevent circular imports
        from cms.utils.placeholder import get_static_placeholders

        return get_static_placeholders(self.get_template(), context)

    def get_xframe_options(self):
        """ Finds X_FRAME_OPTION from tree if inherited """
        xframe_options = self.xframe_options or self.X_FRAME_OPTIONS_INHERIT

        if xframe_options != self.X_FRAME_OPTIONS_INHERIT:
            return xframe_options

        # Ignore those pages which just inherit their value
        ancestors = self.get_ancestor_pages().order_by('-node__path')
        ancestors = ancestors.exclude(xframe_options=self.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

class PageType(Page):

    class Meta:
        proxy = True
        default_permissions = []

    @classmethod
    def get_root_page(cls, site):
        pages = Page.objects.on_site(site).filter(
            node__depth=1,
            is_page_type=True,
        )
        return pages.first()

    def is_potential_home(self):
        return False
meeb commented 3 years ago

Yes, by definition a static site must be static, therefore request.user objects don't make any sense. You can't login() or anything to a static site. You'll need to remove anything that dynamically changes page content to render it as static HTML.

mateoreyes commented 3 years ago

You're right, but I can't understand why users will generate a problem in Django CMS, and not in Django.

If I limit my details function to this, I get the same error.

# views.py
def details(request, slug):
    page = get_page_from_request(request, use_path=slug)
    return  render_page(request, page, current_language=request_language, slug=slug)
meeb commented 3 years ago

Well, I'd guess that your get_page_from_request() method contains a reference to request.user as well then?

mateoreyes commented 3 years ago

Yes

# page.py
def get_page_from_request(request, use_path=None, clean_path=None):
    """
    Gets the current page from a request object.

    URLs can be of the following form (this should help understand the code):
    http://server.whatever.com/<some_path>/"pages-root"/some/page/slug

    <some_path>: This can be anything, and should be stripped when resolving
        pages names. This means the CMS is not installed at the root of the
        server's URLs.
    "pages-root" This is the root of Django urls for the CMS. It is, in essence
        an empty page slug (slug == '')

    The page slug can then be resolved to a Page model object
    """
    from cms.utils.page_permissions import user_can_view_page_draft

    if hasattr(request, '_current_page_cache'):
        # The following is set by CurrentPageMiddleware
        return request._current_page_cache

    if clean_path is None:
        clean_path = not bool(use_path)

    draft = use_draft(request)
    preview = 'preview' in request.GET
    path = request.path_info if use_path is None else use_path

    if clean_path:
        pages_root = reverse("pages-root")

        if path.startswith(pages_root):
            path = path[len(pages_root):]

        # strip any final slash
        if path.endswith("/"):
            path = path[:-1]

    site = get_current_site()
    page = get_page_from_path(site, path, preview, draft)

    if draft and page and not user_can_view_page_draft(request.user, page):
        page = get_page_from_path(site, path, preview, draft=False)

    # For public pages, check if any parent is hidden due to published dates
    # In this case the selected page is not reachable
    if page and not draft:
        now = timezone.now()
        unpublished_ancestors = (
            page
            .get_ancestor_pages()
            .filter(
                Q(publication_date__gt=now)
                | Q(publication_end_date__lt=now),
            )
        )
        if unpublished_ancestors.exists():
            page = None
    return page
meeb commented 3 years ago

Well, you've pretty much answered your own issue then 😃 Either way, this isn't an issue with django-distill, just the code you're trying to wrap with it being dynamic. Obviously "can a user view this page" type checks don't make logical sense for a static page generator. Try commenting out or working around your access check for static page generation.

meeb commented 3 years ago

Closing as not an issue with django-distill.

meeb commented 3 years ago

Also just for some issue creating etiquette feedback, pasting your entire applications source code into an issue probably isn't a great idea. Put it up somewhere and link to specific lines, most people won't audit your codebase for you 😄