1# -*- coding: utf-8 -*-
2import copy
3from collections import OrderedDict
4from logging import getLogger
5from os.path import join
6
7from django.contrib.sites.models import Site
8from django.urls import reverse
9from django.db import models
10from django.db.models.base import ModelState
11from django.db.models.functions import Concat
12from django.utils.encoding import force_text, python_2_unicode_compatible
13from django.utils.functional import cached_property
14from django.utils.timezone import now
15from django.utils.translation import (
16    get_language,
17    override as force_language,
18    ugettext_lazy as _,
19)
20
21from cms import constants
22from cms.constants import PUBLISHER_STATE_DEFAULT, PUBLISHER_STATE_PENDING, PUBLISHER_STATE_DIRTY, TEMPLATE_INHERITANCE_MAGIC
23from cms.exceptions import PublicIsUnmodifiable, PublicVersionNeeded, LanguageError
24from cms.models.managers import PageManager, PageNodeManager
25from cms.utils import i18n
26from cms.utils.compat import DJANGO_1_11
27from cms.utils.conf import get_cms_setting
28from cms.utils.page import get_clean_username
29from cms.utils.i18n import get_current_language
30
31from menus.menu_pool import menu_pool
32
33from treebeard.mp_tree import MP_Node
34
35
36logger = getLogger(__name__)
37
38
39@python_2_unicode_compatible
40class TreeNode(MP_Node):
41
42    parent = models.ForeignKey(
43        'self',
44        on_delete=models.CASCADE,
45        blank=True,
46        null=True,
47        related_name='children',
48        db_index=True,
49    )
50    site = models.ForeignKey(
51        Site,
52        on_delete=models.CASCADE,
53        verbose_name=_("site"),
54        related_name='djangocms_nodes',
55        db_index=True,
56    )
57
58    objects = PageNodeManager()
59
60    class Meta:
61        app_label = 'cms'
62        ordering = ('path',)
63        default_permissions = []
64
65    def __str__(self):
66        return self.path
67
68    @cached_property
69    def item(self):
70        return self.get_item()
71
72    def get_item(self):
73        # Paving the way...
74        return Page.objects.get(node=self, publisher_is_draft=True)
75
76    @property
77    def is_branch(self):
78        return bool(self.numchild)
79
80    def get_ancestor_paths(self):
81        paths = frozenset(
82            self.path[0:pos]
83            for pos in range(0, len(self.path), self.steplen)[1:]
84        )
85        return paths
86
87    def add_child(self, **kwargs):
88        if len(kwargs) == 1 and 'instance' in kwargs:
89            kwargs['instance'].parent = self
90        else:
91            kwargs['parent'] = self
92        return super(TreeNode, self).add_child(**kwargs)
93
94    def add_sibling(self, pos=None, *args, **kwargs):
95        if len(kwargs) == 1 and 'instance' in kwargs:
96            kwargs['instance'].parent_id = self.parent_id
97        else:
98            kwargs['parent_id'] = self.parent_id
99        return super(TreeNode, self).add_sibling(pos, *args, **kwargs)
100
101    def update(self, **data):
102        cls = self.__class__
103        cls.objects.filter(pk=self.pk).update(**data)
104
105        for field, value in data.items():
106            setattr(self, field, value)
107        return
108
109    def get_cached_ancestors(self):
110        if self._has_cached_hierarchy():
111            return self._ancestors
112        return []
113
114    def get_cached_descendants(self):
115        if self._has_cached_hierarchy():
116            return self._descendants
117        return []
118
119    def _reload(self):
120        """
121        Reload a page node from the database
122        """
123        return self.__class__.objects.get(pk=self.pk)
124
125    def _has_cached_hierarchy(self):
126        return hasattr(self, '_descendants') and hasattr(self, '_ancestors')
127
128    def _set_hierarchy(self, nodes, ancestors=None):
129        if self.is_branch:
130            self._descendants = [node for node in nodes
131                           if node.path.startswith(self.path)
132                           and node.depth > self.depth]
133        else:
134            self._descendants = []
135
136        if self.is_root():
137            self._ancestors = []
138        else:
139            self._ancestors = ancestors
140
141        children = (node for node in self._descendants
142                    if node.depth == self.depth + 1)
143
144        for child in children:
145            child._set_hierarchy(self._descendants, ancestors=([self] + self._ancestors))
146
147
148@python_2_unicode_compatible
149class Page(models.Model):
150    """
151    A simple hierarchical page model
152    """
153    LIMIT_VISIBILITY_IN_MENU_CHOICES = (
154        (constants.VISIBILITY_USERS, _('for logged in users only')),
155        (constants.VISIBILITY_ANONYMOUS, _('for anonymous users only')),
156    )
157    TEMPLATE_DEFAULT = TEMPLATE_INHERITANCE_MAGIC if get_cms_setting('TEMPLATE_INHERITANCE') else get_cms_setting('TEMPLATES')[0][0]
158
159    X_FRAME_OPTIONS_INHERIT = constants.X_FRAME_OPTIONS_INHERIT
160    X_FRAME_OPTIONS_DENY = constants.X_FRAME_OPTIONS_DENY
161    X_FRAME_OPTIONS_SAMEORIGIN = constants.X_FRAME_OPTIONS_SAMEORIGIN
162    X_FRAME_OPTIONS_ALLOW = constants.X_FRAME_OPTIONS_ALLOW
163    X_FRAME_OPTIONS_CHOICES = (
164        (constants.X_FRAME_OPTIONS_INHERIT, _('Inherit from parent page')),
165        (constants.X_FRAME_OPTIONS_DENY, _('Deny')),
166        (constants.X_FRAME_OPTIONS_SAMEORIGIN, _('Only this website')),
167        (constants.X_FRAME_OPTIONS_ALLOW, _('Allow'))
168    )
169
170    template_choices = [(x, _(y)) for x, y in get_cms_setting('TEMPLATES')]
171
172    created_by = models.CharField(
173        _("created by"), max_length=constants.PAGE_USERNAME_MAX_LENGTH,
174        editable=False)
175    changed_by = models.CharField(
176        _("changed by"), max_length=constants.PAGE_USERNAME_MAX_LENGTH,
177        editable=False)
178    creation_date = models.DateTimeField(auto_now_add=True)
179    changed_date = models.DateTimeField(auto_now=True)
180
181    publication_date = models.DateTimeField(_("publication date"), null=True, blank=True, help_text=_(
182        'When the page should go live. Status must be "Published" for page to go live.'), db_index=True)
183    publication_end_date = models.DateTimeField(_("publication end date"), null=True, blank=True,
184                                                help_text=_('When to expire the page. Leave empty to never expire.'),
185                                                db_index=True)
186    #
187    # Please use toggle_in_navigation() instead of affecting this property
188    # directly so that the cms page cache can be invalidated as appropriate.
189    #
190    in_navigation = models.BooleanField(_("in navigation"), default=True, db_index=True)
191    soft_root = models.BooleanField(_("soft root"), db_index=True, default=False,
192                                    help_text=_("All ancestors will not be displayed in the navigation"))
193    reverse_id = models.CharField(_("id"), max_length=40, db_index=True, blank=True, null=True, help_text=_(
194        "A unique identifier that is used with the page_url templatetag for linking to this page"))
195    navigation_extenders = models.CharField(_("attached menu"), max_length=80, db_index=True, blank=True, null=True)
196    template = models.CharField(_("template"), max_length=100, choices=template_choices,
197                                help_text=_('The template used to render the content.'),
198                                default=TEMPLATE_DEFAULT)
199
200    login_required = models.BooleanField(_("login required"), default=False)
201    limit_visibility_in_menu = models.SmallIntegerField(_("menu visibility"), default=None, null=True, blank=True,
202                                                        choices=LIMIT_VISIBILITY_IN_MENU_CHOICES, db_index=True,
203                                                        help_text=_("limit when this page is visible in the menu"))
204    is_home = models.BooleanField(editable=False, db_index=True, default=False)
205    application_urls = models.CharField(_('application'), max_length=200, blank=True, null=True, db_index=True)
206    application_namespace = models.CharField(_('application instance name'), max_length=200, blank=True, null=True)
207
208    # Placeholders (plugins)
209    placeholders = models.ManyToManyField('cms.Placeholder', editable=False)
210
211    # Publisher fields
212    publisher_is_draft = models.BooleanField(default=True, editable=False, db_index=True)
213    # This is misnamed - the one-to-one relation is populated on both ends
214    publisher_public = models.OneToOneField(
215        'self',
216        on_delete=models.CASCADE,
217        related_name='publisher_draft',
218        null=True,
219        editable=False,
220    )
221    languages = models.CharField(max_length=255, editable=False, blank=True, null=True)
222
223    # X Frame Options for clickjacking protection
224    xframe_options = models.IntegerField(
225        choices=X_FRAME_OPTIONS_CHOICES,
226        default=get_cms_setting('DEFAULT_X_FRAME_OPTIONS'),
227    )
228
229    # Flag that marks a page as page-type
230    is_page_type = models.BooleanField(default=False)
231
232    node = models.ForeignKey(
233        TreeNode,
234        related_name='cms_pages',
235        on_delete=models.CASCADE,
236    )
237
238    # Managers
239    objects = PageManager()
240
241    class Meta:
242        default_permissions = ('add', 'change', 'delete')
243        permissions = (
244            ('view_page', 'Can view page'),
245            ('publish_page', 'Can publish page'),
246            ('edit_static_placeholder', 'Can edit static placeholders'),
247        )
248        unique_together = ('node', 'publisher_is_draft')
249        verbose_name = _('page')
250        verbose_name_plural = _('pages')
251        app_label = 'cms'
252
253    def __init__(self, *args, **kwargs):
254        super(Page, self).__init__(*args, **kwargs)
255        self.title_cache = {}
256
257    def __str__(self):
258        try:
259            title = self.get_menu_title(fallback=True)
260        except LanguageError:
261            try:
262                title = self.title_set.all()[0]
263            except IndexError:
264                title = None
265        if title is None:
266            title = u""
267        return force_text(title)
268
269    def __repr__(self):
270        display = '<{module}.{class_name} id={id} is_draft={is_draft} object at {location}>'.format(
271            module=self.__module__,
272            class_name=self.__class__.__name__,
273            id=self.pk,
274            is_draft=self.publisher_is_draft,
275            location=hex(id(self)),
276        )
277        return display
278
279    def _clear_node_cache(self):
280        if DJANGO_1_11:
281            if hasattr(self, '_node_cache'):
282                del self._node_cache
283        else:
284            if Page.node.is_cached(self):
285                Page.node.field.delete_cached_value(self)
286
287    def _clear_internal_cache(self):
288        self.title_cache = {}
289        self._clear_node_cache()
290
291        if hasattr(self, '_prefetched_objects_cache'):
292            del self._prefetched_objects_cache
293
294    @cached_property
295    def parent_page(self):
296        return self.get_parent_page()
297
298    def set_as_homepage(self, user=None):
299        """
300        Sets the given page as the homepage.
301        Updates the title paths for all affected pages.
302        Returns the old home page (if any).
303        """
304        assert self.publisher_is_draft
305
306        if user:
307            changed_by = get_clean_username(user)
308        else:
309            changed_by = constants.SCRIPT_USERNAME
310
311        changed_date = now()
312
313        try:
314            old_home = self.__class__.objects.get(
315                is_home=True,
316                node__site=self.node.site_id,
317                publisher_is_draft=True,
318            )
319        except self.__class__.DoesNotExist:
320            old_home_tree = []
321        else:
322            old_home.update(
323                draft_only=False,
324                is_home=False,
325                changed_by=changed_by,
326                changed_date=changed_date,
327            )
328            old_home_tree = old_home._set_title_root_path()
329
330        self.update(
331            draft_only=False,
332            is_home=True,
333            changed_by=changed_by,
334            changed_date=changed_date,
335        )
336        new_home_tree = self._remove_title_root_path()
337        return (new_home_tree, old_home_tree)
338
339    def _update_title_path(self, language):
340        parent_page = self.get_parent_page()
341
342        if parent_page:
343            base = parent_page.get_path(language, fallback=True)
344        else:
345            base = ''
346
347        title_obj = self.get_title_obj(language, fallback=False)
348        title_obj.path = title_obj.get_path_for_base(base)
349        title_obj.save()
350
351    def _update_title_path_recursive(self, language):
352        assert self.publisher_is_draft
353        from cms.models import Title
354
355        if self.node.is_leaf() or language not in self.get_languages():
356            return
357
358        pages = self.get_child_pages()
359        base = self.get_path(language, fallback=True)
360
361        if base:
362            new_path = Concat(models.Value(base), models.Value('/'), models.F('slug'))
363        else:
364            # User is moving the homepage
365            new_path = models.F('slug')
366
367        (Title
368         .objects
369         .filter(language=language, page__in=pages)
370         .exclude(has_url_overwrite=True)
371         .update(path=new_path))
372
373        for child in pages.filter(title_set__language=language).iterator():
374            child._update_title_path_recursive(language)
375
376    def _set_title_root_path(self):
377        from cms.models import Title
378
379        node_tree = TreeNode.get_tree(self.node)
380        page_tree = self.__class__.objects.filter(node__in=node_tree)
381        translations = Title.objects.filter(page__in=page_tree, has_url_overwrite=False)
382
383        for language, slug in self.title_set.values_list('language', 'slug'):
384            # Update the translations for all descendants of this page
385            # to include this page's slug as its path prefix
386            (translations
387             .filter(language=language)
388             .update(path=Concat(models.Value(slug), models.Value('/'), 'path')))
389
390            # Explicitly update this page's path to match its slug
391            # Doing this is cheaper than a TRIM call to remove the "/" characters
392            if self.publisher_public_id:
393                # include the public translation
394                current_translations = Title.objects.filter(page__in=[self.pk, self.publisher_public_id])
395            else:
396                current_translations = self.title_set.all()
397            current_translations.filter(language=language).update(path=slug)
398        return page_tree
399
400    def _remove_title_root_path(self):
401        from cms.models import Title
402
403        node_tree = TreeNode.get_tree(self.node)
404        page_tree = self.__class__.objects.filter(node__in=node_tree)
405        translations = Title.objects.filter(page__in=page_tree, has_url_overwrite=False)
406
407        for language, slug in self.title_set.values_list('language', 'slug'):
408            # Use 2 because of 1 indexing plus the fact we need to trim
409            # the "/" character.
410            trim_count = len(slug) + 2
411            sql_func = models.Func(
412                models.F('path'),
413                models.Value(trim_count),
414                function='substr',
415            )
416            (translations
417             .filter(language=language, path__startswith=slug)
418             .update(path=sql_func))
419        return page_tree
420
421    def is_dirty(self, language):
422        state = self.get_publisher_state(language)
423        return state == PUBLISHER_STATE_DIRTY or state == PUBLISHER_STATE_PENDING
424
425    def is_potential_home(self):
426        """
427        Encapsulates logic for determining if this page is eligible to be set
428        as `is_home`. This is a public method so that it can be accessed in the
429        admin for determining whether to enable the "Set as home" menu item.
430        :return: Boolean
431        """
432        assert self.publisher_is_draft
433        # Only root nodes are eligible for homepage
434        return not self.is_home and bool(self.node.is_root())
435
436    def get_absolute_url(self, language=None, fallback=True):
437        if not language:
438            language = get_current_language()
439
440        with force_language(language):
441            if self.is_home:
442                return reverse('pages-root')
443            path = self.get_path(language, fallback) or self.get_slug(language, fallback)
444            return reverse('pages-details-by-slug', kwargs={"slug": path})
445
446    def get_public_url(self, language=None, fallback=True):
447        """
448        Returns the URL of the published version of the current page.
449        Returns empty string if the page is not published.
450        """
451        try:
452            return self.get_public_object().get_absolute_url(language, fallback)
453        except:
454            return ''
455
456    def get_draft_url(self, language=None, fallback=True):
457        """
458        Returns the URL of the draft version of the current page.
459        Returns empty string if the draft page is not available.
460        """
461        try:
462            return self.get_draft_object().get_absolute_url(language, fallback)
463        except:
464            return ''
465
466    def set_tree_node(self, site, target=None, position='first-child'):
467        assert self.publisher_is_draft
468        assert position in ('last-child', 'first-child', 'left', 'right')
469
470        new_node = TreeNode(site=site)
471
472        if target is None:
473            self.node = TreeNode.add_root(instance=new_node)
474        elif position == 'first-child' and target.is_branch:
475            self.node = target.get_first_child().add_sibling(pos='left', instance=new_node)
476        elif position in ('last-child', 'first-child'):
477            self.node = target.add_child(instance=new_node)
478        else:
479            self.node = target.add_sibling(pos=position, instance=new_node)
480
481    def move_page(self, target_node, position='first-child'):
482        """
483        Called from admin interface when page is moved. Should be used on
484        all the places which are changing page position. Used like an interface
485        to django-treebeard, but after move is done page_moved signal is fired.
486
487        Note for issue #1166: url conflicts are handled by updated
488        check_title_slugs, overwrite_url on the moved page don't need any check
489        as it remains the same regardless of the page position in the tree
490        """
491        assert self.publisher_is_draft
492        assert isinstance(target_node, TreeNode)
493
494        inherited_template = self.template == constants.TEMPLATE_INHERITANCE_MAGIC
495
496        if inherited_template and target_node.is_root() and position in ('left', 'right'):
497            # The page is being moved to a root position.
498            # Explicitly set the inherited template on the page
499            # to keep all plugins / placeholders.
500            self.update(refresh=False, template=self.get_template())
501
502        # Don't use a cached node. Always get a fresh one.
503        self._clear_node_cache()
504
505        # Runs the SQL updates on the treebeard fields
506        self.node.move(target_node, position)
507
508        if position in ('first-child', 'last-child'):
509            parent_id = target_node.pk
510        else:
511            # moving relative to sibling
512            # or to the root of the tree
513            parent_id = target_node.parent_id
514        # Runs the SQL updates on the parent field
515        self.node.update(parent_id=parent_id)
516
517        # Clear the cached node once again to trigger a db query
518        # on access.
519        self._clear_node_cache()
520
521        # Update the descendants to "PENDING"
522        # If the target (parent) page is not published
523        # and the page being moved is published.
524        titles = (
525            self
526            .title_set
527            .filter(language__in=self.get_languages())
528            .values_list('language', 'published')
529        )
530        parent_page = self.get_parent_page()
531
532        if parent_page:
533            parent_titles = (
534                parent_page
535                .title_set
536                .exclude(publisher_state=PUBLISHER_STATE_PENDING)
537                .values_list('language', 'published')
538            )
539            parent_titles_by_language = dict(parent_titles)
540        else:
541            parent_titles_by_language = {}
542
543        for language, published in titles:
544            parent_is_published = parent_titles_by_language.get(language)
545
546            # Update draft title path
547            self._update_title_path(language)
548            self._update_title_path_recursive(language)
549
550            if published and parent_is_published:
551                # this looks redundant but it's necessary
552                # for all the descendants of the page being
553                # moved to be set to the correct state.
554                self.publisher_public._update_title_path(language)
555                self.mark_as_published(language)
556                self.mark_descendants_as_published(language)
557            elif published and parent_page:
558                # page is published but it's parent is not
559                # mark the page being moved (source) as "pending"
560                self.mark_as_pending(language)
561                # mark all descendants of source as "pending"
562                self.mark_descendants_pending(language)
563            elif published:
564                self.publisher_public._update_title_path(language)
565                self.mark_as_published(language)
566                self.mark_descendants_as_published(language)
567        self.clear_cache(menu=True)
568        return self
569
570    def _copy_titles(self, target, language, published):
571        """
572        Copy the title matching language to a new page (which must have a pk).
573        :param target: The page where the new title should be stored
574        """
575        source_title = self.title_set.get(language=language)
576
577        try:
578            target_title_id = (
579                target
580                .title_set
581                .filter(language=language)
582                .values_list('pk', flat=True)[0]
583            )
584        except IndexError:
585            target_title_id = None
586
587        source_title_id = source_title.pk
588
589        # If an old title exists, overwrite. Otherwise create new
590        source_title.pk = target_title_id
591        source_title.page = target
592        source_title.publisher_is_draft = target.publisher_is_draft
593        source_title.publisher_public_id = source_title_id
594        source_title.published = published
595        source_title._publisher_keep_state = True
596
597        if published:
598            source_title.publisher_state = PUBLISHER_STATE_DEFAULT
599        else:
600            source_title.publisher_state = PUBLISHER_STATE_PENDING
601        source_title.save()
602        return source_title
603
604    def _clear_placeholders(self, language=None):
605        from cms.models import CMSPlugin
606
607        placeholders = list(self.get_placeholders())
608        placeholder_ids = (placeholder.pk for placeholder in placeholders)
609        plugins = CMSPlugin.objects.filter(placeholder__in=placeholder_ids)
610
611        if language:
612            plugins = plugins.filter(language=language)
613        models.query.QuerySet.delete(plugins)
614        return placeholders
615
616    def _copy_contents(self, target, language):
617        """
618        Copy all the plugins to a new page.
619        :param target: The page where the new content should be stored
620        """
621        cleared_placeholders = target._clear_placeholders(language)
622        cleared_placeholders_by_slot = {pl.slot: pl for pl in cleared_placeholders}
623
624        for placeholder in self.get_placeholders():
625            try:
626                target_placeholder = cleared_placeholders_by_slot[placeholder.slot]
627            except KeyError:
628                target_placeholder = target.placeholders.create(
629                    slot=placeholder.slot,
630                    default_width=placeholder.default_width,
631                )
632
633            placeholder.copy_plugins(target_placeholder, language=language)
634
635    def _copy_attributes(self, target, clean=False):
636        """
637        Copy all page data to the target. This excludes parent and other values
638        that are specific to an exact instance.
639        :param target: The Page to copy the attributes to
640        """
641        if not clean:
642            target.publication_date = self.publication_date
643            target.publication_end_date = self.publication_end_date
644            target.reverse_id = self.reverse_id
645        target.changed_by = self.changed_by
646        target.login_required = self.login_required
647        target.in_navigation = self.in_navigation
648        target.soft_root = self.soft_root
649        target.limit_visibility_in_menu = self.limit_visibility_in_menu
650        target.navigation_extenders = self.navigation_extenders
651        target.application_urls = self.application_urls
652        target.application_namespace = self.application_namespace
653        target.template = self.template
654        target.xframe_options = self.xframe_options
655        target.is_page_type = self.is_page_type
656
657    def copy(self, site, parent_node=None, language=None,
658             translations=True, permissions=False, extensions=True):
659        from cms.utils.page import get_available_slug
660
661        if parent_node:
662            new_node = parent_node.add_child(site=site)
663            parent_page = parent_node.item
664        else:
665            new_node = TreeNode.add_root(site=site)
666            parent_page = None
667
668        new_page = copy.copy(self)
669        new_page._state = ModelState()
670        new_page._clear_internal_cache()
671        new_page.pk = None
672        new_page.node = new_node
673        new_page.publisher_public_id = None
674        new_page.is_home = False
675        new_page.reverse_id = None
676        new_page.publication_date = None
677        new_page.publication_end_date = None
678        new_page.languages = ''
679        new_page.save()
680
681        # Have the node remember its page.
682        # This is done to save some queries
683        # when the node's descendants are copied.
684        new_page.node.__dict__['item'] = new_page
685
686        if language and translations:
687            translations = self.title_set.filter(language=language)
688        elif translations:
689            translations = self.title_set.all()
690        else:
691            translations = self.title_set.none()
692
693        # copy titles of this page
694        for title in translations:
695            title = copy.copy(title)
696            title.pk = None
697            title.page = new_page
698            title.published = False
699            title.publisher_public = None
700
701            if parent_page:
702                base = parent_page.get_path(title.language)
703                path = '%s/%s' % (base, title.slug) if base else title.slug
704            else:
705                base = ''
706                path = title.slug
707
708            title.slug = get_available_slug(site, path, title.language)
709            title.path = '%s/%s' % (base, title.slug) if base else title.slug
710            title.save()
711
712            new_page.title_cache[title.language] = title
713        new_page.update_languages([trans.language for trans in translations])
714
715        # copy the placeholders (and plugins on those placeholders!)
716        for placeholder in self.placeholders.iterator():
717            new_placeholder = copy.copy(placeholder)
718            new_placeholder.pk = None
719            new_placeholder.save()
720            new_page.placeholders.add(new_placeholder)
721            placeholder.copy_plugins(new_placeholder, language=language)
722
723        if extensions:
724            from cms.extensions import extension_pool
725            extension_pool.copy_extensions(self, new_page)
726
727        # copy permissions if requested
728        if permissions and get_cms_setting('PERMISSION'):
729            permissions = self.pagepermission_set.iterator()
730            permissions_new = []
731
732            for permission in permissions:
733                permission.pk = None
734                permission.page = new_page
735                permissions_new.append(permission)
736
737            if permissions_new:
738                new_page.pagepermission_set.bulk_create(permissions_new)
739        return new_page
740
741    def copy_with_descendants(self, target_node=None, position=None,
742                              copy_permissions=True, target_site=None):
743        """
744        Copy a page [ and all its descendants to a new location ]
745        """
746        if not self.publisher_is_draft:
747            raise PublicIsUnmodifiable("copy page is not allowed for public pages")
748
749        if position in ('first-child', 'last-child'):
750            parent_node = target_node
751        elif target_node:
752            parent_node = target_node.parent
753        else:
754            parent_node = None
755
756        if target_site is None:
757            target_site = parent_node.site if parent_node else self.node.site
758
759        # Evaluate the descendants queryset BEFORE copying the page.
760        # Otherwise, if the page is copied and pasted on itself, it will duplicate.
761        descendants = list(
762            self.get_descendant_pages()
763            .select_related('node')
764            .prefetch_related('title_set')
765        )
766        new_root_page = self.copy(target_site, parent_node=parent_node)
767        new_root_node = new_root_page.node
768
769        if target_node and position in ('first-child'):
770            # target node is a parent and user has requested to
771            # insert the new page as its first child
772            new_root_node.move(target_node, position)
773            new_root_node.refresh_from_db(fields=('path', 'depth'))
774
775        if target_node and position in ('left', 'last-child'):
776            # target node is a sibling
777            new_root_node.move(target_node, position)
778            new_root_node.refresh_from_db(fields=('path', 'depth'))
779
780        nodes_by_id = {self.node.pk: new_root_node}
781
782        for page in descendants:
783            parent = nodes_by_id[page.node.parent_id]
784            new_page = page.copy(
785                target_site,
786                parent_node=parent,
787                translations=True,
788                permissions=copy_permissions,
789            )
790            nodes_by_id[page.node_id] = new_page.node
791        return new_root_page
792
793    def delete(self, *args, **kwargs):
794        TreeNode.get_tree(self.node).delete_fast()
795
796        if self.node.parent_id:
797            (TreeNode
798             .objects
799             .filter(pk=self.node.parent_id)
800             .update(numchild=models.F('numchild') - 1))
801        self.clear_cache(menu=True)
802
803    def delete_translations(self, language=None):
804        if language is None:
805            languages = self.get_languages()
806        else:
807            languages = [language]
808
809        self.title_set.filter(language__in=languages).delete()
810
811        for language in languages:
812            self.mark_descendants_pending(language)
813
814    def save(self, **kwargs):
815        # delete template cache
816        if hasattr(self, '_template_cache'):
817            delattr(self, '_template_cache')
818
819        created = not bool(self.pk)
820        if self.reverse_id == "":
821            self.reverse_id = None
822        if self.application_namespace == "":
823            self.application_namespace = None
824        from cms.utils.permissions import get_current_user_name
825
826        self.changed_by = get_current_user_name()
827
828        if created:
829            self.created_by = self.changed_by
830        super(Page, self).save(**kwargs)
831
832    def save_base(self, *args, **kwargs):
833        """Overridden save_base. If an instance is draft, and was changed, mark
834        it as dirty.
835
836        Dirty flag is used for changed nodes identification when publish method
837        takes place. After current changes are published, state is set back to
838        PUBLISHER_STATE_DEFAULT (in publish method).
839        """
840        keep_state = getattr(self, '_publisher_keep_state', None)
841        if self.publisher_is_draft and not keep_state and self.is_new_dirty():
842            self.title_set.all().update(publisher_state=PUBLISHER_STATE_DIRTY)
843        if keep_state:
844            delattr(self, '_publisher_keep_state')
845        return super(Page, self).save_base(*args, **kwargs)
846
847    def update(self, refresh=False, draft_only=True, **data):
848        assert self.publisher_is_draft
849
850        cls = self.__class__
851
852        if not draft_only and self.publisher_public_id:
853            ids = [self.pk, self.publisher_public_id]
854            cls.objects.filter(pk__in=ids).update(**data)
855        else:
856            cls.objects.filter(pk=self.pk).update(**data)
857
858        if refresh:
859            return self.reload()
860        else:
861            for field, value in data.items():
862                setattr(self, field, value)
863        return
864
865    def update_translations(self, language=None, **data):
866        if language:
867            translations = self.title_set.filter(language=language)
868        else:
869            translations = self.title_set.all()
870        return translations.update(**data)
871
872    def has_translation(self, language):
873        return self.title_set.filter(language=language).exists()
874
875    def is_new_dirty(self):
876        if self.pk:
877            fields = [
878                'publication_date', 'publication_end_date', 'in_navigation', 'soft_root', 'reverse_id',
879                'navigation_extenders', 'template', 'login_required', 'limit_visibility_in_menu'
880            ]
881            try:
882                old_page = Page.objects.get(pk=self.pk)
883            except Page.DoesNotExist:
884                return True
885            for field in fields:
886                old_val = getattr(old_page, field)
887                new_val = getattr(self, field)
888                if not old_val == new_val:
889                    return True
890            return False
891        return True
892
893    def is_published(self, language, force_reload=False):
894        title_obj = self.get_title_obj(language, fallback=False, force_reload=force_reload)
895        return title_obj.published and title_obj.publisher_state != PUBLISHER_STATE_PENDING
896
897    def toggle_in_navigation(self, set_to=None):
898        '''
899        Toggles (or sets) in_navigation and invalidates the cms page cache
900        '''
901        old = self.in_navigation
902        if set_to in [True, False]:
903            self.in_navigation = set_to
904        else:
905            self.in_navigation = not self.in_navigation
906        self.save()
907
908        # If there was a change, invalidate the cms page cache
909        if self.in_navigation != old:
910            self.clear_cache()
911        return self.in_navigation
912
913    def get_publisher_state(self, language, force_reload=False):
914        try:
915            return self.get_title_obj(language, False, force_reload=force_reload).publisher_state
916        except AttributeError:
917            return None
918
919    def set_publisher_state(self, language, state, published=None):
920        title = self.title_set.get(language=language)
921        title.publisher_state = state
922        if published is not None:
923            title.published = published
924        title._publisher_keep_state = True
925        title.save()
926        if language in self.title_cache:
927            self.title_cache[language].publisher_state = state
928        return title
929
930    def publish(self, language):
931        """
932        :returns: True if page was successfully published.
933        """
934        from cms.utils.permissions import get_current_user_name
935
936        # Publish can only be called on draft pages
937        if not self.publisher_is_draft:
938            raise PublicIsUnmodifiable('The public instance cannot be published. Use draft.')
939
940        if not self._publisher_can_publish(language):
941            return False
942
943        if self.publisher_public_id:
944            public_page = Page.objects.get(pk=self.publisher_public_id)
945            public_languages = public_page.get_languages()
946        else:
947            public_page = Page(created_by=self.created_by)
948            public_languages = [language]
949
950        self._copy_attributes(public_page, clean=False)
951
952        if language not in public_languages:
953            public_languages.append(language)
954
955        # TODO: Get rid of the current user thread hack
956        public_page.changed_by = get_current_user_name()
957        public_page.is_home = self.is_home
958        public_page.publication_date = self.publication_date or now()
959        public_page.publisher_public = self
960        public_page.publisher_is_draft = False
961        public_page.languages = ','.join(public_languages)
962        public_page.node = self.node
963        public_page.save()
964
965        # Copy the page translation (title) matching language
966        # into a "public" version.
967        public_title = self._copy_titles(public_page, language, published=True)
968
969        # Ensure this draft page points to its public version
970        self.update(
971            draft_only=True,
972            changed_by=public_page.changed_by,
973            publisher_public=public_page,
974            publication_date=public_page.publication_date,
975        )
976
977        # Set the draft page translation matching language
978        # to point to its public version.
979        # Its important for draft to be published even if its state
980        # is pending.
981        self.update_translations(
982            language,
983            published=True,
984            publisher_public=public_title,
985            publisher_state=PUBLISHER_STATE_DEFAULT,
986        )
987        self._copy_contents(public_page, language)
988
989        if self.node.is_branch:
990            self.mark_descendants_as_published(language)
991
992        if language in self.title_cache:
993            del self.title_cache[language]
994
995        # fire signal after publishing is done
996        import cms.signals as cms_signals
997
998        cms_signals.post_publish.send(sender=Page, instance=self, language=language)
999
1000        public_page.clear_cache(
1001            language,
1002            menu=True,
1003            placeholder=True,
1004        )
1005        return True
1006
1007    def clear_cache(self, language=None, menu=False, placeholder=False):
1008        from cms.cache import invalidate_cms_page_cache
1009
1010        if get_cms_setting('PAGE_CACHE'):
1011            # Clears all the page caches
1012            invalidate_cms_page_cache()
1013
1014        if placeholder and get_cms_setting('PLACEHOLDER_CACHE'):
1015            assert language, 'language is required when clearing placeholder cache'
1016
1017            placeholders = self.get_placeholders()
1018
1019            for placeholder in placeholders:
1020                placeholder.clear_cache(language, site_id=self.node.site_id)
1021
1022        if menu:
1023            # Clears all menu caches for this page's site
1024            menu_pool.clear(site_id=self.node.site_id)
1025
1026    def unpublish(self, language, site=None):
1027        """
1028        Removes this page from the public site
1029        :returns: True if this page was successfully unpublished
1030        """
1031        # Publish can only be called on draft pages
1032        if not self.publisher_is_draft:
1033            raise PublicIsUnmodifiable('The public instance cannot be unpublished. Use draft.')
1034
1035        self.update_translations(
1036            language,
1037            published=False,
1038            publisher_state=PUBLISHER_STATE_DIRTY,
1039        )
1040
1041        public_page = self.get_public_object()
1042        public_page.update_translations(language, published=False)
1043        public_page._clear_placeholders(language)
1044        public_page.clear_cache(language)
1045
1046        self.mark_descendants_pending(language)
1047
1048        from cms.signals import post_unpublish
1049        post_unpublish.send(sender=Page, instance=self, language=language)
1050
1051        return True
1052
1053    def get_child_pages(self):
1054        nodes = self.node.get_children()
1055        pages = (
1056            self
1057            .__class__
1058            .objects
1059            .filter(
1060                node__in=nodes,
1061                publisher_is_draft=self.publisher_is_draft,
1062            )
1063            .order_by('node__path')
1064        )
1065        return pages
1066
1067    def get_ancestor_pages(self):
1068        nodes = self.node.get_ancestors()
1069        pages = (
1070            self
1071            .__class__
1072            .objects
1073            .filter(
1074                node__in=nodes,
1075                publisher_is_draft=self.publisher_is_draft,
1076            )
1077            .order_by('node__path')
1078        )
1079        return pages
1080
1081    def get_descendant_pages(self):
1082        nodes = self.node.get_descendants()
1083        pages = (
1084            self
1085            .__class__
1086            .objects
1087            .filter(
1088                node__in=nodes,
1089                publisher_is_draft=self.publisher_is_draft,
1090            )
1091            .order_by('node__path')
1092        )
1093        return pages
1094
1095    def get_root(self):
1096        node = self.node
1097        return self.__class__.objects.get(
1098            node__path=node.path[0:node.steplen],
1099            publisher_is_draft=self.publisher_is_draft,
1100        )
1101
1102    def get_parent_page(self):
1103        if not self.node.parent_id:
1104            return None
1105
1106        pages = Page.objects.filter(
1107            node=self.node.parent_id,
1108            publisher_is_draft=self.publisher_is_draft,
1109        )
1110        return pages.select_related('node').first()
1111
1112    def mark_as_pending(self, language):
1113        assert self.publisher_is_draft
1114        assert self.publisher_public_id
1115
1116        self.get_public_object().title_set.filter(language=language).update(published=False)
1117
1118        if self.get_publisher_state(language) == PUBLISHER_STATE_DEFAULT:
1119            # Only change the state if the draft page is published
1120            # and it's state is the default (0), to avoid overriding a dirty state.
1121            self.set_publisher_state(language, state=PUBLISHER_STATE_PENDING)
1122
1123    def mark_descendants_pending(self, language):
1124        from cms.models import Title
1125
1126        if not self.publisher_is_draft:
1127            raise PublicIsUnmodifiable('The public instance cannot be altered. Use draft.')
1128
1129        node_descendants = self.node.get_descendants()
1130        page_descendants = self.__class__.objects.filter(node__in=node_descendants)
1131
1132        if page_descendants.filter(publisher_is_draft=True).exists():
1133            # Only change the state if the draft page is not dirty
1134            # to avoid overriding a dirty state.
1135            Title.objects.filter(
1136                published=True,
1137                language=language,
1138                page__in=page_descendants.filter(publisher_is_draft=True),
1139                publisher_state=PUBLISHER_STATE_DEFAULT,
1140            ).update(publisher_state=PUBLISHER_STATE_PENDING)
1141
1142        if page_descendants.filter(publisher_is_draft=False).exists():
1143            Title.objects.filter(
1144                published=True,
1145                language=language,
1146                page__in=page_descendants.filter(publisher_is_draft=False),
1147            ).update(published=False)
1148
1149    def mark_as_published(self, language):
1150        from cms.models import Title
1151
1152        (Title
1153         .objects
1154         .filter(page=self.publisher_public_id, language=language)
1155         .update(publisher_state=PUBLISHER_STATE_DEFAULT, published=True))
1156
1157        draft = self.get_draft_object()
1158
1159        if draft.get_publisher_state(language) == PUBLISHER_STATE_PENDING:
1160            # A check for pending is necessary because the page might have
1161            # been modified after it was marked as pending.
1162            draft.set_publisher_state(language, PUBLISHER_STATE_DEFAULT)
1163
1164    def mark_descendants_as_published(self, language):
1165        from cms.models import Title
1166
1167        if not self.publisher_is_draft:
1168            raise PublicIsUnmodifiable('The public instance cannot be published. Use draft.')
1169
1170        base = self.get_path(language, fallback=True)
1171        node_children = self.node.get_children()
1172        page_children = self.__class__.objects.filter(node__in=node_children)
1173        page_children_draft = page_children.filter(publisher_is_draft=True)
1174        page_children_public = page_children.filter(publisher_is_draft=False)
1175
1176        # Set public pending titles as published
1177        unpublished_public = Title.objects.filter(
1178            language=language,
1179            page__in=page_children_public,
1180            publisher_public__published=True,
1181        )
1182
1183        if base:
1184            new_path = Concat(models.Value(base), models.Value('/'), models.F('slug'))
1185        else:
1186            # User is moving the homepage
1187            new_path = models.F('slug')
1188
1189        # Update public title paths
1190        unpublished_public.exclude(has_url_overwrite=True).update(path=new_path)
1191
1192        # Set unpublished pending titles to published
1193        unpublished_public.filter(published=False).update(published=True)
1194
1195        # Update drafts
1196        Title.objects.filter(
1197            published=True,
1198            language=language,
1199            page__in=page_children_draft,
1200            publisher_state=PUBLISHER_STATE_PENDING
1201        ).update(publisher_state=PUBLISHER_STATE_DEFAULT)
1202
1203        # Continue publishing descendants, one branch at a time.
1204        published_children = page_children_draft.filter(
1205            title_set__published=True,
1206            title_set__language=language,
1207        )
1208
1209        for child in published_children.iterator():
1210            child.mark_descendants_as_published(language)
1211
1212    def revert_to_live(self, language):
1213        """Revert the draft version to the same state as the public version
1214        """
1215        if not self.publisher_is_draft:
1216            # Revert can only be called on draft pages
1217            raise PublicIsUnmodifiable('The public instance cannot be reverted. Use draft.')
1218
1219        public = self.get_public_object()
1220
1221        if not public:
1222            raise PublicVersionNeeded('A public version of this page is needed')
1223
1224        public._copy_attributes(self)
1225        public._copy_contents(self, language)
1226        public._copy_titles(self, language, public.is_published(language))
1227
1228        self.update_translations(
1229            language,
1230            published=True,
1231            publisher_state=PUBLISHER_STATE_DEFAULT,
1232        )
1233        self._publisher_keep_state = True
1234        self.save()
1235
1236    def get_draft_object(self):
1237        if not self.publisher_is_draft:
1238            return self.publisher_draft
1239        return self
1240
1241    def get_public_object(self):
1242        if not self.publisher_is_draft:
1243            return self
1244        return self.publisher_public
1245
1246    def get_languages(self):
1247        if self.languages:
1248            return sorted(self.languages.split(','))
1249        else:
1250            return []
1251
1252    def remove_language(self, language):
1253        page_languages = self.get_languages()
1254
1255        if language in page_languages:
1256            page_languages.remove(language)
1257            self.update_languages(page_languages)
1258
1259    def update_languages(self, languages):
1260        languages = ",".join(languages)
1261        # Update current instance
1262        self.languages = languages
1263        # Commit. It's important to not call save()
1264        # we'd like to commit only the languages field and without
1265        # any kind of signals.
1266        self.update(draft_only=False, languages=languages)
1267
1268    def get_published_languages(self):
1269        if self.publisher_is_draft:
1270            return self.get_languages()
1271        return sorted([language for language in self.get_languages() if self.is_published(language)])
1272
1273    def set_translations_cache(self):
1274        for translation in self.title_set.all():
1275            self.title_cache.setdefault(translation.language, translation)
1276
1277    def get_path_for_slug(self, slug, language):
1278        if self.is_home:
1279            return ''
1280
1281        if self.parent_page:
1282            base = self.parent_page.get_path(language, fallback=True)
1283            # base can be empty when the parent is a home-page
1284            path = u'%s/%s' % (base, slug) if base else slug
1285        else:
1286            path = slug
1287        return path
1288
1289    # ## Title object access
1290
1291    def get_title_obj(self, language=None, fallback=True, force_reload=False):
1292        """Helper function for accessing wanted / current title.
1293        If wanted title doesn't exists, EmptyTitle instance will be returned.
1294        """
1295        language = self._get_title_cache(language, fallback, force_reload)
1296        if language in self.title_cache:
1297            return self.title_cache[language]
1298        from cms.models.titlemodels import EmptyTitle
1299
1300        return EmptyTitle(language)
1301
1302    def get_title_obj_attribute(self, attrname, language=None, fallback=True, force_reload=False):
1303        """Helper function for getting attribute or None from wanted/current title.
1304        """
1305        try:
1306            attribute = getattr(self.get_title_obj(language, fallback, force_reload), attrname)
1307            return attribute
1308        except AttributeError:
1309            return None
1310
1311    def get_path(self, language=None, fallback=True, force_reload=False):
1312        """
1313        get the path of the page depending on the given language
1314        """
1315        return self.get_title_obj_attribute("path", language, fallback, force_reload)
1316
1317    def get_slug(self, language=None, fallback=True, force_reload=False):
1318        """
1319        get the slug of the page depending on the given language
1320        """
1321        return self.get_title_obj_attribute("slug", language, fallback, force_reload)
1322
1323    def get_title(self, language=None, fallback=True, force_reload=False):
1324        """
1325        get the title of the page depending on the given language
1326        """
1327        return self.get_title_obj_attribute("title", language, fallback, force_reload)
1328
1329    def get_menu_title(self, language=None, fallback=True, force_reload=False):
1330        """
1331        get the menu title of the page depending on the given language
1332        """
1333        menu_title = self.get_title_obj_attribute("menu_title", language, fallback, force_reload)
1334        if not menu_title:
1335            return self.get_title(language, True, force_reload)
1336        return menu_title
1337
1338    def get_placeholders(self):
1339        if not hasattr(self, '_placeholder_cache'):
1340            self._placeholder_cache = self.placeholders.all()
1341        return self._placeholder_cache
1342
1343    def _validate_title(self, title):
1344        from cms.models.titlemodels import EmptyTitle
1345        if isinstance(title, EmptyTitle):
1346            return False
1347        if not title.title or not title.slug:
1348            return False
1349        return True
1350
1351    def get_admin_tree_title(self):
1352        from cms.models.titlemodels import EmptyTitle
1353        language = get_language()
1354
1355        if not self.title_cache:
1356            self.set_translations_cache()
1357
1358        if language not in self.title_cache or not self._validate_title(self.title_cache.get(language, EmptyTitle(language))):
1359            fallback_langs = i18n.get_fallback_languages(language)
1360            found = False
1361            for lang in fallback_langs:
1362                if lang in self.title_cache and self._validate_title(self.title_cache.get(lang, EmptyTitle(lang))):
1363                    found = True
1364                    language = lang
1365            if not found:
1366                language = None
1367                for lang, item in self.title_cache.items():
1368                    if not isinstance(item, EmptyTitle):
1369                        language = lang
1370        if not language:
1371            return _("Empty")
1372        title = self.title_cache[language]
1373        if title.title:
1374            return title.title
1375        if title.page_title:
1376            return title.page_title
1377        if title.menu_title:
1378            return title.menu_title
1379        return title.slug
1380
1381    def get_changed_date(self, language=None, fallback=True, force_reload=False):
1382        """
1383        get when this page was last updated
1384        """
1385        return self.changed_date
1386
1387    def get_changed_by(self, language=None, fallback=True, force_reload=False):
1388        """
1389        get user who last changed this page
1390        """
1391        return self.changed_by
1392
1393    def get_page_title(self, language=None, fallback=True, force_reload=False):
1394        """
1395        get the page title of the page depending on the given language
1396        """
1397        page_title = self.get_title_obj_attribute("page_title", language, fallback, force_reload)
1398
1399        if not page_title:
1400            return self.get_title(language, True, force_reload)
1401        return page_title
1402
1403    def get_meta_description(self, language=None, fallback=True, force_reload=False):
1404        """
1405        get content for the description meta tag for the page depending on the given language
1406        """
1407        return self.get_title_obj_attribute("meta_description", language, fallback, force_reload)
1408
1409    def get_application_urls(self, language=None, fallback=True, force_reload=False):
1410        """
1411        get application urls conf for application hook
1412        """
1413        return self.application_urls
1414
1415    def get_redirect(self, language=None, fallback=True, force_reload=False):
1416        """
1417        get redirect
1418        """
1419        return self.get_title_obj_attribute("redirect", language, fallback, force_reload)
1420
1421    def _get_title_cache(self, language, fallback, force_reload):
1422        if not language:
1423            language = get_language()
1424
1425        force_reload = (force_reload or language not in self.title_cache)
1426
1427        if fallback and not self.title_cache.get(language):
1428            # language can be in the cache but might be an EmptyTitle instance
1429            fallback_langs = i18n.get_fallback_languages(language)
1430            for lang in fallback_langs:
1431                if self.title_cache.get(lang):
1432                    return lang
1433
1434        if force_reload:
1435            from cms.models.titlemodels import Title
1436
1437            titles = Title.objects.filter(page=self)
1438            for title in titles:
1439                self.title_cache[title.language] = title
1440            if self.title_cache.get(language):
1441                return language
1442            else:
1443                if fallback:
1444                    fallback_langs = i18n.get_fallback_languages(language)
1445                    for lang in fallback_langs:
1446                        if self.title_cache.get(lang):
1447                            return lang
1448        return language
1449
1450    def get_template(self):
1451        """
1452        get the template of this page if defined or if closer parent if
1453        defined or DEFAULT_PAGE_TEMPLATE otherwise
1454        """
1455        if hasattr(self, '_template_cache'):
1456            return self._template_cache
1457
1458        if self.template != constants.TEMPLATE_INHERITANCE_MAGIC:
1459            self._template_cache = self.template or get_cms_setting('TEMPLATES')[0][0]
1460            return self._template_cache
1461
1462        templates = (
1463            self
1464            .get_ancestor_pages()
1465            .exclude(template=constants.TEMPLATE_INHERITANCE_MAGIC)
1466            .order_by('-node__path')
1467            .values_list('template', flat=True)
1468        )
1469
1470        try:
1471            self._template_cache = templates[0]
1472        except IndexError:
1473            self._template_cache = get_cms_setting('TEMPLATES')[0][0]
1474        return self._template_cache
1475
1476    def get_template_name(self):
1477        """
1478        get the textual name (2nd parameter in get_cms_setting('TEMPLATES'))
1479        of the template of this page or of the nearest
1480        ancestor. failing to find that, return the name of the default template.
1481        """
1482        template = self.get_template()
1483        for t in get_cms_setting('TEMPLATES'):
1484            if t[0] == template:
1485                return t[1]
1486        return _("default")
1487
1488    def has_view_permission(self, user):
1489        from cms.utils.page_permissions import user_can_view_page
1490        return user_can_view_page(user, page=self)
1491
1492    def has_view_restrictions(self, site):
1493        from cms.models import PagePermission
1494
1495        if get_cms_setting('PERMISSION'):
1496            page = self.get_draft_object()
1497            restrictions = (
1498                PagePermission
1499                .objects
1500                .for_page(page)
1501                .filter(can_view=True)
1502            )
1503            return restrictions.exists()
1504        return False
1505
1506    def has_add_permission(self, user):
1507        """
1508        Has user ability to add page under current page?
1509        """
1510        from cms.utils.page_permissions import user_can_add_subpage
1511        return user_can_add_subpage(user, self)
1512
1513    def has_change_permission(self, user):
1514        from cms.utils.page_permissions import user_can_change_page
1515        return user_can_change_page(user, page=self)
1516
1517    def has_delete_permission(self, user):
1518        from cms.utils.page_permissions import user_can_delete_page
1519        return user_can_delete_page(user, page=self)
1520
1521    def has_delete_translation_permission(self, user, language):
1522        from cms.utils.page_permissions import user_can_delete_page_translation
1523        return user_can_delete_page_translation(user, page=self, language=language)
1524
1525    def has_publish_permission(self, user):
1526        from cms.utils.page_permissions import user_can_publish_page
1527        return user_can_publish_page(user, page=self)
1528
1529    def has_advanced_settings_permission(self, user):
1530        from cms.utils.page_permissions import user_can_change_page_advanced_settings
1531        return user_can_change_page_advanced_settings(user, page=self)
1532
1533    def has_change_permissions_permission(self, user):
1534        """
1535        Has user ability to change permissions for current page?
1536        """
1537        from cms.utils.page_permissions import user_can_change_page_permissions
1538        return user_can_change_page_permissions(user, page=self)
1539
1540    def has_move_page_permission(self, user):
1541        """Has user ability to move current page?
1542        """
1543        from cms.utils.page_permissions import user_can_move_page
1544        return user_can_move_page(user, page=self)
1545
1546    def has_placeholder_change_permission(self, user):
1547        if not self.publisher_is_draft:
1548            return False
1549        return self.has_change_permission(user)
1550
1551    def get_media_path(self, filename):
1552        """
1553        Returns path (relative to MEDIA_ROOT/MEDIA_URL) to directory for storing
1554        page-scope files. This allows multiple pages to contain files with
1555        identical names without namespace issues. Plugins such as Picture can
1556        use this method to initialise the 'upload_to' parameter for File-based
1557        fields. For example:
1558            image = models.ImageField(
1559                _("image"), upload_to=CMSPlugin.get_media_path)
1560
1561        where CMSPlugin.get_media_path calls self.page.get_media_path
1562
1563        This location can be customised using the CMS_PAGE_MEDIA_PATH setting
1564        """
1565        return join(get_cms_setting('PAGE_MEDIA_PATH'), "%d" % self.pk, filename)
1566
1567    def reload(self):
1568        """
1569        Reload a page from the database
1570        """
1571        return self.__class__.objects.get(pk=self.pk)
1572
1573    def _publisher_can_publish(self, language):
1574        """Is parent of this object already published?
1575        """
1576        if self.is_page_type:
1577            return False
1578
1579        if not self.parent_page:
1580            return True
1581
1582        if self.parent_page.publisher_public_id:
1583            return self.parent_page.get_public_object().is_published(language)
1584        return False
1585
1586    def rescan_placeholders(self):
1587        """
1588        Rescan and if necessary create placeholders in the current template.
1589        """
1590        existing = OrderedDict()
1591        placeholders = [pl.slot for pl in self.get_declared_placeholders()]
1592
1593        for placeholder in self.placeholders.all():
1594            if placeholder.slot in placeholders:
1595                existing[placeholder.slot] = placeholder
1596
1597        for placeholder in placeholders:
1598            if placeholder not in existing:
1599                existing[placeholder] = self.placeholders.create(slot=placeholder)
1600        return existing
1601
1602    def get_declared_placeholders(self):
1603        # inline import to prevent circular imports
1604        from cms.utils.placeholder import get_placeholders
1605
1606        return get_placeholders(self.get_template())
1607
1608    def get_declared_static_placeholders(self, context):
1609        # inline import to prevent circular imports
1610        from cms.utils.placeholder import get_static_placeholders
1611
1612        return get_static_placeholders(self.get_template(), context)
1613
1614    def get_xframe_options(self):
1615        """ Finds X_FRAME_OPTION from tree if inherited """
1616        xframe_options = self.xframe_options or self.X_FRAME_OPTIONS_INHERIT
1617
1618        if xframe_options != self.X_FRAME_OPTIONS_INHERIT:
1619            return xframe_options
1620
1621        # Ignore those pages which just inherit their value
1622        ancestors = self.get_ancestor_pages().order_by('-node__path')
1623        ancestors = ancestors.exclude(xframe_options=self.X_FRAME_OPTIONS_INHERIT)
1624
1625        # Now just give me the clickjacking setting (not anything else)
1626        xframe_options = ancestors.values_list('xframe_options', flat=True)
1627
1628        try:
1629            return xframe_options[0]
1630        except IndexError:
1631            return None
1632
1633
1634class PageType(Page):
1635
1636    class Meta:
1637        proxy = True
1638        default_permissions = []
1639
1640    @classmethod
1641    def get_root_page(cls, site):
1642        pages = Page.objects.on_site(site).filter(
1643            node__depth=1,
1644            is_page_type=True,
1645        )
1646        return pages.first()
1647
1648    def is_potential_home(self):
1649        return False
1650