class CMSPluginBase(with_metaclass(CMSPluginBaseMetaclass, admin.ModelAdmin)): name = "" module = _("Generic") # To be overridden in child classes form = None change_form_template = "admin/cms/page/plugin/change_form.html" frontend_edit_template = 'cms/toolbar/plugin.html' # Should the plugin be rendered in the admin? admin_preview = False render_template = None # Should the plugin be rendered at all, or doesn't it have any output? render_plugin = True model = CMSPlugin text_enabled = False page_only = False allow_children = False child_classes = None require_parent = False parent_classes = None disable_child_plugin = False cache = get_cms_setting('PLUGIN_CACHE') opts = {} action_options = { PLUGIN_MOVE_ACTION: { 'requires_reload': False }, PLUGIN_COPY_ACTION: { 'requires_reload': True }, } def __init__(self, model=None, admin_site=None): if admin_site: super(CMSPluginBase, self).__init__(self.model, admin_site) self.object_successfully_changed = False # variables will be overwritten in edit_view, so we got required self.cms_plugin_instance = None self.placeholder = None self.page = None def render(self, context, instance, placeholder): context['instance'] = instance context['placeholder'] = placeholder return context @property def parent(self): return self.cms_plugin_instance.parent def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): """ We just need the popup interface here """ context.update({ 'preview': not "no_preview" in request.GET, 'is_popup': True, 'plugin': self.cms_plugin_instance, 'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'), }) return super(CMSPluginBase, self).render_change_form(request, context, add, change, form_url, obj) def has_add_permission(self, request, *args, **kwargs): """Permission handling change - if user is allowed to change the page he must be also allowed to add/change/delete plugins.. Not sure if there will be plugin permission requirement in future, but if, then this must be changed. """ return self.cms_plugin_instance.has_change_permission(request) has_delete_permission = has_change_permission = has_add_permission def save_model(self, request, obj, form, change): """ Override original method, and add some attributes to obj This have to be made, because if object is newly created, he must know where he lives. Attributes from cms_plugin_instance have to be assigned to object, if is cms_plugin_instance attribute available. """ if getattr(self, "cms_plugin_instance"): # assign stuff to object fields = self.cms_plugin_instance._meta.fields for field in fields: # assign all the fields - we can do this, because object is # subclassing cms_plugin_instance (one to one relation) value = getattr(self.cms_plugin_instance, field.name) setattr(obj, field.name, value) # remember the saved object self.saved_object = obj return super(CMSPluginBase, self).save_model(request, obj, form, change) def response_change(self, request, obj): """ Just set a flag, so we know something was changed, and can make new version if reversion installed. New version will be created in admin.views.edit_plugin """ self.object_successfully_changed = True return super(CMSPluginBase, self).response_change(request, obj) def response_add(self, request, obj, **kwargs): """ Just set a flag, so we know something was changed, and can make new version if reversion installed. New version will be created in admin.views.edit_plugin """ self.object_successfully_changed = True if not DJANGO_1_4: post_url_continue = reverse('admin:cms_page_edit_plugin', args=(obj._get_pk_val(),), current_app=self.admin_site.name) kwargs.setdefault('post_url_continue', post_url_continue) return super(CMSPluginBase, self).response_add(request, obj, **kwargs) def log_addition(self, request, object): pass def log_change(self, request, object, message): pass def log_deletion(self, request, object, object_repr): pass def icon_src(self, instance): """ Overwrite this if text_enabled = True Return the URL for an image to be used for an icon for this plugin instance in a text editor. """ return "" def icon_alt(self, instance): """ Overwrite this if necessary if text_enabled = True Return the 'alt' text to be used for an icon representing the plugin object in a text editor. """ return "%s - %s" % (force_unicode(self.name), force_unicode(instance)) def get_fieldsets(self, request, obj=None): """ Same as from base class except if there are no fields, show an info message. """ fieldsets = super(CMSPluginBase, self).get_fieldsets(request, obj) for name, data in fieldsets: if data.get('fields'): # if fieldset with non-empty fields is found, return fieldsets return fieldsets if self.inlines: return [] # if plugin has inlines but no own fields return empty fieldsets to remove empty white fieldset try: # if all fieldsets are empty (assuming there is only one fieldset then) add description fieldsets[0][1]['description'] = _('There are no further settings for this plugin. Please press save.') except KeyError: pass return fieldsets def get_child_classes(self, slot, page): template = None if page: template = page.template ## config overrides.. ph_conf = get_placeholder_conf('child_classes', slot, template, default={}) child_classes = ph_conf.get(self.__class__.__name__, None) if child_classes: return child_classes if self.child_classes: return self.child_classes else: from cms.plugin_pool import plugin_pool installed_plugins = plugin_pool.get_all_plugins(slot, page) return [cls.__name__ for cls in installed_plugins] def get_parent_classes(self, slot, page): template = None if page: template = page.template ## config overrides.. ph_conf = get_placeholder_conf('parent_classes', slot, template, default={}) parent_classes = ph_conf.get(self.__class__.__name__, None) if parent_classes: return parent_classes elif self.parent_classes: return self.parent_classes else: return None def get_action_options(self): return self.action_options def requires_reload(self, action): actions = self.get_action_options() reload_required = False if action in actions: options = actions[action] reload_required = options.get('requires_reload', False) return reload_required def get_plugin_urls(self): """ Return URL patterns for which the plugin wants to register views for. """ return [] def plugin_urls(self): return self.get_plugin_urls() plugin_urls = property(plugin_urls) def __repr__(self): return smart_str(self.name) def __str__(self): return self.name #=========================================================================== # Deprecated APIs #=========================================================================== @property def pluginmedia(self): raise Deprecated( "CMSPluginBase.pluginmedia is deprecated in favor of django-sekizai" ) def get_plugin_media(self, request, context, plugin): raise Deprecated( "CMSPluginBase.get_plugin_media is deprecated in favor of django-sekizai" )
class CMSPlugin(with_metaclass(PluginModelBase, MPTTModel)): ''' The base class for a CMS plugin model. When defining a new custom plugin, you should store plugin-instance specific information on a subclass of this class. An example for this would be to store the number of pictures to display in a galery. Two restrictions apply when subclassing this to use in your own models: 1. Subclasses of CMSPlugin *cannot be further subclassed* 2. Subclasses of CMSPlugin cannot define a "text" field. ''' placeholder = models.ForeignKey(Placeholder, editable=False, null=True) parent = models.ForeignKey('self', blank=True, null=True, editable=False) position = models.PositiveSmallIntegerField(_("position"), blank=True, null=True, editable=False) language = models.CharField(_("language"), max_length=15, blank=False, db_index=True, editable=False) plugin_type = models.CharField(_("plugin_name"), max_length=50, db_index=True, editable=False) creation_date = models.DateTimeField(_("creation date"), editable=False, default=timezone.now) changed_date = models.DateTimeField(auto_now=True) level = models.PositiveIntegerField(db_index=True, editable=False) lft = models.PositiveIntegerField(db_index=True, editable=False) rght = models.PositiveIntegerField(db_index=True, editable=False) tree_id = models.PositiveIntegerField(db_index=True, editable=False) child_plugin_instances = None translatable_content_excluded_fields = [] class Meta: app_label = 'cms' class RenderMeta: index = 0 total = 1 text_enabled = False def __reduce__(self): """ Provide pickling support. Normally, this just dispatches to Python's standard handling. However, for models with deferred field loading, we need to do things manually, as they're dynamically created classes and only module-level classes can be pickled by the default path. """ data = self.__dict__ model = self.__class__ # The obvious thing to do here is to invoke super().__reduce__() # for the non-deferred case. Don't do that. # On Python 2.4, there is something wierd with __reduce__, # and as a result, the super call will cause an infinite recursion. # See #10547 and #12121. defers = [] pk_val = None if self._deferred: factory = deferred_class_factory for field in self._meta.fields: if isinstance(self.__class__.__dict__.get(field.attname), DeferredAttribute): defers.append(field.attname) if pk_val is None: # The pk_val and model values are the same for all # DeferredAttribute classes, so we only need to do this # once. obj = self.__class__.__dict__[field.attname] model = obj.model_ref() else: factory = lambda x, y: x return (model_unpickle, (model, defers, factory), data) def __str__(self): return force_unicode(self.pk) def get_plugin_name(self): from cms.plugin_pool import plugin_pool return plugin_pool.get_plugin(self.plugin_type).name def get_short_description(self): instance = self.get_plugin_instance()[0] if instance is not None: return force_unicode(instance) return _("<Empty>") def get_plugin_class(self): from cms.plugin_pool import plugin_pool return plugin_pool.get_plugin(self.plugin_type) def get_plugin_class_instance(self, admin=None): plugin_class = self.get_plugin_class() # needed so we have the same signature as the original ModelAdmin return plugin_class(plugin_class.model, admin) def get_plugin_instance(self, admin=None): plugin = self.get_plugin_class_instance(admin) if hasattr(self, "_inst"): return self._inst, plugin if plugin.model != self.__class__: # and self.__class__ == CMSPlugin: # (if self is actually a subclass, getattr below would break) try: instance = plugin.model.objects.get(cmsplugin_ptr=self) instance._render_meta = self._render_meta except (AttributeError, ObjectDoesNotExist): instance = None else: instance = self self._inst = instance return self._inst, plugin def render_plugin(self, context=None, placeholder=None, admin=False, processors=None): instance, plugin = self.get_plugin_instance() if instance and not (admin and not plugin.admin_preview): if not placeholder or not isinstance(placeholder, Placeholder): placeholder = instance.placeholder placeholder_slot = placeholder.slot current_app = context.current_app if context else None context = PluginContext(context, instance, placeholder, current_app=current_app) context = plugin.render(context, instance, placeholder_slot) request = context.get('request', None) page = None if request: page = request.current_page context['allowed_child_classes'] = plugin.get_child_classes(placeholder_slot, page) if plugin.render_plugin: template = hasattr(instance, 'render_template') and instance.render_template or plugin.render_template if not template: raise ValidationError("plugin has no render_template: %s" % plugin.__class__) else: template = None return render_plugin(context, instance, placeholder, template, processors, context.current_app) else: from cms.middleware.toolbar import toolbar_plugin_processor if processors and toolbar_plugin_processor in processors: if not placeholder: placeholder = self.placeholder current_app = context.current_app if context else None context = PluginContext(context, self, placeholder, current_app=current_app) template = None return render_plugin(context, self, placeholder, template, processors, context.current_app) return "" def get_media_path(self, filename): pages = self.placeholder.page_set.all() if pages.count(): return pages[0].get_media_path(filename) else: # django 1.0.2 compatibility today = date.today() return os.path.join(get_cms_setting('PAGE_MEDIA_PATH'), str(today.year), str(today.month), str(today.day), filename) @property def page(self): warnings.warn( "Don't use the page attribute on CMSPlugins! CMSPlugins are not " "guaranteed to have a page associated with them!", DontUsePageAttributeWarning) return self.placeholder.page if self.placeholder_id else None def get_instance_icon_src(self): """ Get src URL for instance's icon """ instance, plugin = self.get_plugin_instance() if instance: return plugin.icon_src(instance) else: return u'' def get_instance_icon_alt(self): """ Get alt text for instance's icon """ instance, plugin = self.get_plugin_instance() if instance: return force_unicode(plugin.icon_alt(instance)) else: return u'' def save(self, no_signals=False, *args, **kwargs): if no_signals: # ugly hack because of mptt if DJANGO_1_5: super(CMSPlugin, self).save_base(cls=self.__class__) else: super(CMSPlugin, self).save_base() else: super(CMSPlugin, self).save() def set_base_attr(self, plugin): for attr in ['parent_id', 'placeholder', 'language', 'plugin_type', 'creation_date', 'level', 'lft', 'rght', 'position', 'tree_id']: setattr(plugin, attr, getattr(self, attr)) def copy_plugin(self, target_placeholder, target_language, parent_cache, no_signals=False): """ Copy this plugin and return the new plugin. """ try: plugin_instance, cls = self.get_plugin_instance() except KeyError: # plugin type not found anymore return # set up some basic attributes on the new_plugin new_plugin = CMSPlugin() new_plugin.placeholder = target_placeholder new_plugin.tree_id = None new_plugin.lft = None new_plugin.rght = None new_plugin.level = None # we assign a parent to our new plugin parent_cache[self.pk] = new_plugin if self.parent: parent = parent_cache[self.parent_id] parent = CMSPlugin.objects.get(pk=parent.pk) new_plugin.parent = parent new_plugin.level = None new_plugin.language = target_language new_plugin.plugin_type = self.plugin_type new_plugin.position = self.position if no_signals: from cms.signals import pre_save_plugins signals.pre_save.disconnect(pre_save_plugins, sender=CMSPlugin, dispatch_uid='cms_pre_save_plugin') signals.pre_save.disconnect(pre_save_plugins, sender=CMSPlugin) new_plugin._no_reorder = True new_plugin.save() if plugin_instance: if plugin_instance.__class__ == CMSPlugin: # get a new instance so references do not get mixed up plugin_instance = CMSPlugin.objects.get(pk=plugin_instance.pk) plugin_instance.pk = new_plugin.pk plugin_instance.id = new_plugin.pk plugin_instance.placeholder = target_placeholder plugin_instance.tree_id = new_plugin.tree_id plugin_instance.lft = new_plugin.lft plugin_instance.rght = new_plugin.rght plugin_instance.level = new_plugin.level plugin_instance.cmsplugin_ptr = new_plugin plugin_instance.language = target_language plugin_instance.parent = new_plugin.parent # added to retain the position when creating a public copy of a plugin plugin_instance.position = new_plugin.position plugin_instance.save() old_instance = plugin_instance.__class__.objects.get(pk=self.pk) plugin_instance.copy_relations(old_instance) if no_signals: signals.pre_save.connect(pre_save_plugins, sender=CMSPlugin, dispatch_uid='cms_pre_save_plugin') return new_plugin def post_copy(self, old_instance, new_old_ziplist): """ Handle more advanced cases (eg Text Plugins) after the original is copied """ pass def copy_relations(self, old_instance): """ Handle copying of any relations attached to this plugin. Custom plugins have to do this themselves! """ pass def has_change_permission(self, request): page = self.placeholder.page if self.placeholder else None if page: return page.has_change_permission(request) elif self.placeholder: return self.placeholder.has_change_permission(request) elif self.parent: return self.parent.has_change_permission(request) return False def is_first_in_placeholder(self): return self.position == 0 def is_last_in_placeholder(self): """ WARNING: this is a rather expensive call compared to is_first_in_placeholder! """ return self.placeholder.cmsplugin_set.filter(parent__isnull=True).order_by('-position')[0].pk == self.pk def get_position_in_placeholder(self): """ 1 based position! """ return self.position + 1 def get_breadcrumb(self): from cms.models import Page model = self.placeholder._get_attached_model() if not model: model = Page breadcrumb = [] if not self.parent_id: try: url = force_unicode( reverse("admin:%s_%s_edit_plugin" % (model._meta.app_label, model._meta.module_name), args=[self.pk])) except NoReverseMatch: url = force_unicode( reverse("admin:%s_%s_edit_plugin" % (Page._meta.app_label, Page._meta.module_name), args=[self.pk])) breadcrumb.append({'title': force_unicode(self.get_plugin_name()), 'url': url}) return breadcrumb for parent in self.get_ancestors(False, True): try: url = force_unicode( reverse("admin:%s_%s_edit_plugin" % (model._meta.app_label, model._meta.module_name), args=[parent.pk])) except NoReverseMatch: url = force_unicode( reverse("admin:%s_%s_edit_plugin" % (Page._meta.app_label, Page._meta.module_name), args=[parent.pk])) breadcrumb.append({'title': force_unicode(parent.get_plugin_name()), 'url': url}) return breadcrumb def get_breadcrumb_json(self): result = json.dumps(self.get_breadcrumb()) result = mark_safe(result) return result def num_children(self): if self.child_plugin_instances: return len(self.child_plugin_instances) def notify_on_autoadd(self, request, conf): """ Method called when we auto add this plugin via default_plugins in CMS_PLACEHOLDER_CONF. Some specific plugins may have some special stuff to do when they are auto added. """ pass def notify_on_autoadd_children(self, request, conf, children): """ Method called when we auto add children to this plugin via default_plugins/<plugin>/children in CMS_PLACEHOLDER_CONF. Some specific plugins may have some special stuff to do when we add children to them. ie : TextPlugin must update its content to add HTML tags to be able to see his children in WYSIWYG. """ pass def get_translatable_content(self): fields = [] for field in self._meta.fields: if ((isinstance(field, models.CharField) or isinstance(field, models.TextField)) and not field.choices and field.editable and field.name not in self.translatable_content_excluded_fields and field): fields.append(field) translatable_fields = {} for field in fields: content = getattr(self, field.name) if content: translatable_fields[field.name] = content return translatable_fields def set_translatable_content(self, fields): for field, value in fields.items(): setattr(self, field, value) self.save() # verify that all fields have been set for field, value in fields.items(): if getattr(self, field) != value: return False return True
class Page(with_metaclass(PageMetaClass, MPTTModel)): """ A simple hierarchical page model """ LIMIT_VISIBILITY_IN_MENU_CHOICES = ( (1, _('for logged in users only')), (2, _('for anonymous users only')), ) PUBLISHER_STATE_DEFAULT = 0 PUBLISHER_STATE_DIRTY = 1 PUBLISHER_STATE_DELETE = 2 # Page was marked published, but some of page parents are not. PUBLISHER_STATE_PENDING = 4 template_choices = [(x, _(y)) for x, y in get_cms_setting('TEMPLATES')] created_by = models.CharField(_("created by"), max_length=70, editable=False) changed_by = models.CharField(_("changed by"), max_length=70, editable=False) parent = models.ForeignKey('self', null=True, blank=True, related_name='children', db_index=True) 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) 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= _("An 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) published = models.BooleanField(_("is published"), blank=True) template = models.CharField( _("template"), max_length=100, choices=template_choices, help_text=_('The template used to render the content.'), default=TEMPLATE_INHERITANCE_MAGIC) site = models.ForeignKey( Site, help_text=_('The site the page is accessible at.'), verbose_name=_("site")) 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")) application_urls = models.CharField(_('application'), max_length=200, blank=True, null=True, db_index=True) application_namespace = models.CharField(_('application namespace'), max_length=200, blank=True, null=True) level = models.PositiveIntegerField(db_index=True, editable=False) lft = models.PositiveIntegerField(db_index=True, editable=False) rght = models.PositiveIntegerField(db_index=True, editable=False) tree_id = models.PositiveIntegerField(db_index=True, editable=False) # Placeholders (plugins) placeholders = models.ManyToManyField(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', related_name='publisher_draft', null=True, editable=False) publisher_state = models.SmallIntegerField(default=0, editable=False, db_index=True) # If the draft is loaded from a reversion version save the revision id here. revision_id = models.PositiveIntegerField(default=0, editable=False) # Managers objects = PageManager() permissions = PagePermissionsPermissionManager() class Meta: permissions = ( ('view_page', 'Can view page'), ('publish_page', 'Can publish page'), ) unique_together = (("publisher_is_draft", "application_namespace"), ) verbose_name = _('page') verbose_name_plural = _('pages') ordering = ('tree_id', 'lft') app_label = 'cms' class PublisherMeta: exclude_fields_append = [ 'id', 'publisher_is_draft', 'publisher_public', 'publisher_state', 'moderator_state', 'placeholders', 'lft', 'rght', 'tree_id', 'parent' ] def __str__(self): title = self.get_menu_title(fallback=True) if title is None: title = u"" return force_unicode(title) def __repr__(self): # This is needed to solve the infinite recursion when # adding new pages. return object.__repr__(self) def is_dirty(self): return self.publisher_state == self.PUBLISHER_STATE_DIRTY def get_absolute_url(self, language=None, fallback=True): 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 move_page(self, target, 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 mptt, 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 """ # do not mark the page as dirty after page moves self._publisher_keep_state = True # readability counts :) is_inherited_template = self.template == constants.TEMPLATE_INHERITANCE_MAGIC # make sure move_page does not break when using INHERIT template # and moving to a top level position if (position in ('left', 'right') and not target.parent and is_inherited_template): self.template = self.get_template() self.move_to(target, position) # fire signal import cms.signals as cms_signals cms_signals.page_moved.send(sender=Page, instance=self) self.save() # always save the page after move, because of publisher # check the slugs page_utils.check_title_slugs(self) # Make sure to update the slug and path of the target page. page_utils.check_title_slugs(target) if self.publisher_public_id: # Ensure we have up to date mptt properties public_page = Page.objects.get(pk=self.publisher_public_id) # Ensure that the page is in the right position and save it public_page = self._publisher_save_public(public_page) cms_signals.page_moved.send(sender=Page, instance=public_page) public_page.save() page_utils.check_title_slugs(public_page) def _copy_titles(self, target): """ Copy all the titles to a new page (which must have a pk). :param target: The page where the new titles should be stored """ old_titles = dict(target.title_set.values_list('language', 'pk')) for title in self.title_set.all(): # If an old title exists, overwrite. Otherwise create new title.pk = old_titles.pop(title.language, None) title.page = target title.save() if old_titles: from .titlemodels import Title Title.objects.filter(id__in=old_titles.values()).delete() def _copy_contents(self, target): """ Copy all the plugins to a new page. :param target: The page where the new content should be stored """ # TODO: Make this into a "graceful" copy instead of deleting and overwriting # copy the placeholders (and plugins on those placeholders!) CMSPlugin.objects.filter(placeholder__page=target).delete() for ph in self.placeholders.all(): plugins = ph.get_plugins_list() try: ph = target.placeholders.get(slot=ph.slot) except Placeholder.DoesNotExist: ph.pk = None # make a new instance ph.save() target.placeholders.add(ph) # update the page copy if plugins: copy_plugins_to(plugins, ph) def _copy_attributes(self, target): """ 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 """ target.publication_date = self.publication_date target.publication_end_date = self.publication_end_date target.in_navigation = self.in_navigation target.login_required = self.login_required target.limit_visibility_in_menu = self.limit_visibility_in_menu target.soft_root = self.soft_root target.reverse_id = self.reverse_id target.navigation_extenders = self.navigation_extenders target.application_urls = self.application_urls target.application_namespace = self.application_namespace target.template = self.template target.site_id = self.site_id def copy_page(self, target, site, position='first-child', copy_permissions=True): """ Copy a page [ and all its descendants to a new location ] Doesn't checks for add page permissions anymore, this is done in PageAdmin. Note: public_copy was added in order to enable the creation of a copy for creating the public page during the publish operation as it sets the publisher_is_draft=False. Note for issue #1166: when copying pages there is no need to check for conflicting URLs as pages are copied unpublished. """ page_copy = None pages = [self] + list(self.get_descendants().order_by('-rght')) site_reverse_ids = Page.objects.filter( site=site, reverse_id__isnull=False).values_list('reverse_id', flat=True) if target: target.old_pk = -1 if position == "first-child": tree = [target] elif target.parent_id: tree = [target.parent] else: tree = [] else: tree = [] if tree: tree[0].old_pk = tree[0].pk first = True # loop over all affected pages (self is included in descendants) for page in pages: titles = list(page.title_set.all()) # get all current placeholders (->plugins) placeholders = list(page.placeholders.all()) origin_id = page.id # create a copy of this page by setting pk = None (=new instance) page.old_pk = page.pk page.pk = None page.level = None page.rght = None page.lft = None page.tree_id = None page.published = False page.publisher_public_id = None # only set reverse_id on standard copy if page.reverse_id in site_reverse_ids: page.reverse_id = None if first: first = False if tree: page.parent = tree[0] else: page.parent = None page.insert_at(target, position) else: count = 1 found = False for prnt in tree: if prnt.old_pk == page.parent_id: page.parent = prnt tree = tree[0:count] found = True break count += 1 if not found: page.parent = None tree.append(page) page.site = site page.save() # copy permissions if necessary if get_cms_setting('PERMISSION') and copy_permissions: from cms.models.permissionmodels import PagePermission for permission in PagePermission.objects.filter( page__id=origin_id): permission.pk = None permission.page = page permission.save() # copy titles of this page for title in titles: title.pk = None # setting pk = None creates a new instance title.page = page # create slug-copy for standard copy title.slug = page_utils.get_available_slug(title) title.save() # copy the placeholders (and plugins on those placeholders!) for ph in placeholders: plugins = ph.get_plugins_list() try: ph = page.placeholders.get(slot=ph.slot) except Placeholder.DoesNotExist: ph.pk = None # make a new instance ph.save() page.placeholders.add(ph) # update the page copy page_copy = page if plugins: copy_plugins_to(plugins, ph) # invalidate the menu for this site menu_pool.clear(site_id=site.pk) return page_copy # return the page_copy or None def save(self, no_signals=False, commit=True, **kwargs): """ Args: commit: True if model should be really saved """ # delete template cache if hasattr(self, '_template_cache'): delattr(self, '_template_cache') created = not bool(self.pk) # Published pages should always have a publication date # if the page is published we set the publish date if not set yet. if self.publication_date is None and self.published: self.publication_date = timezone.now() - timedelta(seconds=5) if self.reverse_id == "": self.reverse_id = None if self.application_namespace == "": self.application_namespace = None from cms.utils.permissions import _thread_locals user = getattr(_thread_locals, "user", None) if user: self.changed_by = user.username else: self.changed_by = "script" if created: self.created_by = self.changed_by if commit: if no_signals: # ugly hack because of mptt self.save_base(cls=self.__class__, **kwargs) else: super(Page, self).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: self.publisher_state = self.PUBLISHER_STATE_DIRTY if keep_state: delattr(self, '_publisher_keep_state') ret = super(Page, self).save_base(*args, **kwargs) return ret def publish(self): """Overrides Publisher method, because there may be some descendants, which are waiting for parent to publish, so publish them if possible. :returns: True if page was successfully published. """ # Publish can only be called on draft pages if not self.publisher_is_draft: raise PublicIsUnmodifiable( 'The public instance cannot be published. Use draft.') # publish, but only if all parents are published!! published = None if not self.pk: self.save() if not self.parent_id: self.clear_home_pk_cache() if self._publisher_can_publish(): if self.publisher_public_id: # Ensure we have up to date mptt properties public_page = Page.objects.get(pk=self.publisher_public_id) else: public_page = Page(created_by=self.created_by) self._copy_attributes(public_page) # we need to set relate this new public copy to its draft page (self) public_page.publisher_public = self public_page.publisher_is_draft = False # Ensure that the page is in the right position and save it public_page = self._publisher_save_public(public_page) public_page.published = (public_page.parent_id is None or public_page.parent.published) public_page.save() # The target page now has a pk, so can be used as a target self._copy_titles(public_page) self._copy_contents(public_page) # invalidate the menu for this site menu_pool.clear(site_id=self.site_id) # taken from Publisher - copy_page needs to call self._publisher_save_public(copy) for mptt insertion # insert_at() was maybe calling _create_tree_space() method, in this # case may tree_id change, so we must update tree_id from db first # before save if getattr(self, 'tree_id', None): me = self._default_manager.get(pk=self.pk) self.tree_id = me.tree_id self.publisher_public = public_page published = True else: # Nothing left to do pass if self.publisher_public and self.publisher_public.published: self.publisher_state = Page.PUBLISHER_STATE_DEFAULT else: self.publisher_state = Page.PUBLISHER_STATE_PENDING self.published = True self._publisher_keep_state = True self.save() # If we are publishing, this page might have become a "home" which # would change the path if self.is_home(): for title in self.title_set.all(): if title.path != '': title.save() # clean moderation log self.pagemoderatorstate_set.all().delete() if not published: # was not published, escape return # Check if there are some children which are waiting for parents to # become published. publish_set = self.get_descendants().filter( published=True).select_related('publisher_public') for page in publish_set: if page.publisher_public: if page.publisher_public.parent.published: if not page.publisher_public.published: page.publisher_public.published = True page.publisher_public.save() if page.publisher_state == Page.PUBLISHER_STATE_PENDING: page.publisher_state = Page.PUBLISHER_STATE_DEFAULT page._publisher_keep_state = True page.save() elif page.publisher_state == Page.PUBLISHER_STATE_PENDING: page.publish() # fire signal after publishing is done import cms.signals as cms_signals cms_signals.post_publish.send(sender=Page, instance=self) return published def unpublish(self): """ 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.') # First, make sure we are in the correct state self.published = False self.save() public_page = self.get_public_object() if public_page: public_page.published = False public_page.save() # Go through all children of our public instance descendants = public_page.get_descendants() for child in descendants: child.published = False child.save() draft = child.publisher_public if (draft and draft.published and draft.publisher_state == Page.PUBLISHER_STATE_DEFAULT): draft.publisher_state = Page.PUBLISHER_STATE_PENDING draft._publisher_keep_state = True draft.save() return True def revert(self): """Revert the draft version to the same state as the public version """ # Revert can only be called on draft pages if not self.publisher_is_draft: raise PublicIsUnmodifiable( 'The public instance cannot be reverted. Use draft.') if not self.publisher_public: # TODO: Issue an error return public = self.publisher_public public._copy_titles(self) if self.parent != (self.publisher_public.parent_id and self.publisher_public.parent.publisher_draft): # We don't send the signals here self.move_to(public.parent.publisher_draft) public._copy_contents(self) public._copy_attributes(self) self.published = True self.publisher_state = self.PUBLISHER_STATE_DEFAULT self._publisher_keep_state = True self.revision_id = 0 self.save() # clean moderation log self.pagemoderatorstate_set.all().delete() def delete(self): """Mark public instance for deletion and delete draft. """ placeholders = self.placeholders.all() for ph in placeholders: plugin = CMSPlugin.objects.filter(placeholder=ph) plugin.delete() ph.delete() if self.publisher_public_id: # mark the public instance for deletion self.publisher_public.publisher_state = self.PUBLISHER_STATE_DELETE self.publisher_public.save() super(Page, self).delete() def delete_with_public(self): """ Assuming this page and all its descendants have been marked for deletion, recursively deletes the entire set of pages including the public instance. """ descendants = list(self.get_descendants().order_by('level')) descendants.reverse() descendants.append(self) # Get all pages that are children of any public page that would be deleted public_children = Page.objects.public().filter( parent__publisher_public__in=descendants) public_pages = Page.objects.public().filter( publisher_public__in=descendants) if set(public_children).difference(public_pages): raise PermissionDenied('There are pages that would be orphaned. ' 'Publish their move requests first.') for page in descendants: placeholders = list(page.placeholders.all()) if page.publisher_public_id: placeholders = placeholders + list( page.publisher_public.placeholders.all()) plugins = CMSPlugin.objects.filter(placeholder__in=placeholders) plugins.delete() for ph in placeholders: ph.delete() if page.publisher_public_id: page.publisher_public.delete() super(Page, page).delete() 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): """ get the list of all existing languages for this page """ from cms.models.titlemodels import Title if not hasattr(self, "all_languages"): self.all_languages = list( sorted( Title.objects.filter(page=self).values_list( "language", flat=True).distinct())) return self.all_languages def get_cached_ancestors(self, ascending=True): if ascending: if not hasattr(self, "ancestors_ascending"): self.ancestors_ascending = list(self.get_ancestors(ascending)) return self.ancestors_ascending else: if not hasattr(self, "ancestors_descending"): self.ancestors_descending = list(self.get_ancestors(ascending)) return self.ancestors_descending # ## Title object access def get_title_obj(self, language=None, fallback=True, version_id=None, force_reload=False): """Helper function for accessing wanted / current title. If wanted title doesn't exists, EmptyTitle instance will be returned. If fallback=False is used, titlemodels.Title.DoesNotExist will be raised when a language does not exist. """ language = self._get_title_cache(language, fallback, version_id, force_reload) if language in self.title_cache: return self.title_cache[language] from cms.models.titlemodels import EmptyTitle return EmptyTitle() def get_title_obj_attribute(self, attrname, language=None, fallback=True, version_id=None, force_reload=False): """Helper function for getting attribute or None from wanted/current title. """ try: attribute = getattr( self.get_title_obj(language, fallback, version_id, force_reload), attrname) return attribute except AttributeError: return None def get_path(self, language=None, fallback=True, version_id=None, force_reload=False): """ get the path of the page depending on the given language """ return self.get_title_obj_attribute("path", language, fallback, version_id, force_reload) def get_slug(self, language=None, fallback=True, version_id=None, force_reload=False): """ get the slug of the page depending on the given language """ return self.get_title_obj_attribute("slug", language, fallback, version_id, force_reload) def get_title(self, language=None, fallback=True, version_id=None, force_reload=False): """ get the title of the page depending on the given language """ return self.get_title_obj_attribute("title", language, fallback, version_id, force_reload) def get_menu_title(self, language=None, fallback=True, version_id=None, 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, version_id, force_reload) if not menu_title: return self.get_title(language, True, version_id, force_reload) return menu_title def get_changed_date(self, language=None, fallback=True, version_id=None, force_reload=False): """ get when this page was last updated """ return self.changed_date def get_changed_by(self, language=None, fallback=True, version_id=None, force_reload=False): """ get user who last changed this page """ return self.changed_by def get_page_title(self, language=None, fallback=True, version_id=None, 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, version_id, force_reload) if not page_title: return self.get_title(language, True, version_id, force_reload) return page_title def get_meta_description(self, language=None, fallback=True, version_id=None, 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, version_id, force_reload) def get_application_urls(self, language=None, fallback=True, version_id=None, force_reload=False): """ get application urls conf for application hook """ return self.application_urls def get_redirect(self, language=None, fallback=True, version_id=None, force_reload=False): """ get redirect """ return self.get_title_obj_attribute("redirect", language, fallback, version_id, force_reload) def _get_title_cache(self, language, fallback, version_id, force_reload): if not language: language = get_language() load = False if not hasattr(self, "title_cache") or force_reload: load = True self.title_cache = {} elif not language in self.title_cache: if fallback: fallback_langs = i18n.get_fallback_languages(language) for lang in fallback_langs: if lang in self.title_cache: return lang load = True if load: from cms.models.titlemodels import Title if version_id: from reversion.models import Version version = get_object_or_404(Version, pk=version_id) revs = [ related_version.object_version for related_version in version.revision.version_set.all() ] for rev in revs: obj = rev.object if obj.__class__ == Title: self.title_cache[obj.language] = obj else: title = Title.objects.get_title(self, language, language_fallback=fallback) if title: self.title_cache[title.language] = title language = title.language 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 template = None if self.template: if self.template != constants.TEMPLATE_INHERITANCE_MAGIC: template = self.template else: try: template = self.get_ancestors(ascending=True).exclude( template=constants.TEMPLATE_INHERITANCE_MAGIC ).values_list('template', flat=True)[0] except IndexError: pass if not template: template = get_cms_setting('TEMPLATES')[0][0] self._template_cache = template return template 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, request): from cms.models.permissionmodels import PagePermission, GlobalPagePermission from cms.utils.plugins import current_site if not self.publisher_is_draft: return self.publisher_draft.has_view_permission(request) # does any restriction exist? # inherited and direct is_restricted = PagePermission.objects.for_page(page=self).filter( can_view=True).exists() if request.user.is_authenticated(): site = current_site(request) global_perms_q = Q(can_view=True) & Q( Q(sites__in=[site]) | Q(sites__isnull=True)) global_view_perms = GlobalPagePermission.objects.with_user( request.user).filter(global_perms_q).exists() # a global permission was given to the request's user if global_view_perms: return True elif not is_restricted: if ((get_cms_setting('PUBLIC_FOR') == 'all') or (get_cms_setting('PUBLIC_FOR') == 'staff' and request.user.is_staff)): return True # a restricted page and an authenticated user elif is_restricted: opts = self._meta codename = '%s.view_%s' % (opts.app_label, opts.object_name.lower()) user_perm = request.user.has_perm(codename) generic_perm = self.has_generic_permission(request, "view") return (user_perm or generic_perm) else: #anonymous user if is_restricted or not get_cms_setting('PUBLIC_FOR') == 'all': # anyonymous user, page has restriction and global access is permitted return False else: # anonymous user, no restriction saved in database return True # Authenticated user # Django wide auth perms "can_view" or cms auth perms "can_view" opts = self._meta codename = '%s.view_%s' % (opts.app_label, opts.object_name.lower()) return (request.user.has_perm(codename) or self.has_generic_permission(request, "view")) def has_change_permission(self, request): opts = self._meta if request.user.is_superuser: return True return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) and \ self.has_generic_permission(request, "change") def has_delete_permission(self, request): opts = self._meta if request.user.is_superuser: return True return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) and \ self.has_generic_permission(request, "delete") def has_publish_permission(self, request): if request.user.is_superuser: return True opts = self._meta return request.user.has_perm(opts.app_label + '.' + "publish_page") and \ self.has_generic_permission(request, "publish") has_moderate_permission = has_publish_permission def has_advanced_settings_permission(self, request): return self.has_generic_permission(request, "advanced_settings") def has_change_permissions_permission(self, request): """ Has user ability to change permissions for current page? """ return self.has_generic_permission(request, "change_permissions") def has_add_permission(self, request): """ Has user ability to add page under current page? """ return self.has_generic_permission(request, "add") def has_move_page_permission(self, request): """Has user ability to move current page? """ return self.has_generic_permission(request, "move_page") def has_generic_permission(self, request, perm_type): """ Return true if the current user has permission on the page. Return the string 'All' if the user has all rights. """ att_name = "permission_%s_cache" % perm_type if not hasattr(self, "permission_user_cache") or not hasattr(self, att_name) \ or request.user.pk != self.permission_user_cache.pk: from cms.utils.permissions import has_generic_permission self.permission_user_cache = request.user setattr( self, att_name, has_generic_permission(self.id, request.user, perm_type, self.site_id)) if getattr(self, att_name): self.permission_edit_cache = True return getattr(self, att_name) def is_home(self): if self.parent_id: return False else: try: return self.home_pk_cache == self.pk except NoHomeFound: pass return False def get_home_pk_cache(self): attr = "%s_home_pk_cache_%s" % (self.publisher_is_draft and "draft" or "public", self.site_id) if getattr(self, attr, None) is None: setattr(self, attr, self.get_object_queryset().get_home(self.site).pk) return getattr(self, attr) def set_home_pk_cache(self, value): attr = "%s_home_pk_cache_%s" % (self.publisher_is_draft and "draft" or "public", self.site_id) setattr(self, attr, value) home_pk_cache = property(get_home_pk_cache, set_home_pk_cache) def clear_home_pk_cache(self): self.home_pk_cache = None 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.id, filename) def last_page_states(self): """Returns last five page states, if they exist, optimized, calls sql query only if some states available """ result = getattr(self, '_moderator_state_cache', None) if result is None: result = list( self.pagemoderatorstate_set.all().order_by('created')) self._moderator_state_cache = result return result[:5] def delete_requested(self): """ Checks whether there are any delete requests for this page. Uses the same cache as last_page_states to minimize DB requests """ from cms.models import PageModeratorState result = getattr(self, '_moderator_state_cache', None) if result is None: return self.pagemoderatorstate_set.get_delete_actions().exists() for state in result: if state.action == PageModeratorState.ACTION_DELETE: return True return False def is_public_published(self): """Returns true if public model is published. """ if hasattr(self, '_public_published_cache'): # if it was cached in change list, return cached value return self._public_published_cache # If we have a public version it will be published as well. # If it isn't published, it should be deleted. return self.published and self.publisher_public_id and self.publisher_public.published def reload(self): """ Reload a page from the database """ return Page.objects.get(pk=self.pk) def get_object_queryset(self): """Returns smart queryset depending on object type - draft / public """ qs = self.__class__.objects return self.publisher_is_draft and qs.drafts() or qs.public( ).published() def _publisher_can_publish(self): """Is parent of this object already published? """ if self.parent_id: try: return bool(self.parent.publisher_public_id) except AttributeError: raise MpttPublisherCantPublish return True def get_next_filtered_sibling(self, **filters): """Very similar to original mptt method, but adds support for filters. Returns this model instance's next sibling in the tree, or ``None`` if it doesn't have a next sibling. """ opts = self._mptt_meta if self.is_root_node(): filters.update({ '%s__isnull' % opts.parent_attr: True, '%s__gt' % opts.tree_id_attr: getattr(self, opts.tree_id_attr), }) else: filters.update({ opts.parent_attr: getattr(self, '%s_id' % opts.parent_attr), '%s__gt' % opts.left_attr: getattr(self, opts.right_attr), }) # publisher stuff filters.update({'publisher_is_draft': self.publisher_is_draft}) # multisite filters.update({'site__id': self.site_id}) sibling = None try: sibling = self._tree_manager.filter(**filters)[0] except IndexError: pass return sibling def get_previous_filtered_sibling(self, **filters): """Very similar to original mptt method, but adds support for filters. Returns this model instance's previous sibling in the tree, or ``None`` if it doesn't have a previous sibling. """ opts = self._mptt_meta if self.is_root_node(): filters.update({ '%s__isnull' % opts.parent_attr: True, '%s__lt' % opts.tree_id_attr: getattr(self, opts.tree_id_attr), }) order_by = '-%s' % opts.tree_id_attr else: filters.update({ opts.parent_attr: getattr(self, '%s_id' % opts.parent_attr), '%s__lt' % opts.right_attr: getattr(self, opts.left_attr), }) order_by = '-%s' % opts.right_attr # publisher stuff filters.update({'publisher_is_draft': self.publisher_is_draft}) # multisite filters.update({'site__id': self.site_id}) sibling = None try: sibling = self._tree_manager.filter( **filters).order_by(order_by)[0] except IndexError: pass return sibling def _publisher_save_public(self, obj): """Mptt specific stuff before the object can be saved, overrides original publisher method. Args: obj - public variant of `self` to be saved. """ public_parent = self.parent.publisher_public if self.parent_id else None filters = dict(publisher_public__isnull=False) if public_parent: filters['publisher_public__parent__in'] = [public_parent] else: filters['publisher_public__parent__isnull'] = True prev_sibling = self.get_previous_filtered_sibling(**filters) public_prev_sib = prev_sibling.publisher_public if prev_sibling else None if not self.publisher_public_id: # first time published # is there anybody on left side? if public_prev_sib: obj.insert_at(public_prev_sib, position='right', save=False) else: if public_parent: obj.insert_at(public_parent, position='first-child', save=False) else: # check if object was moved / structural tree change prev_public_sibling = obj.get_previous_filtered_sibling() if self.level != obj.level or \ public_parent != obj.parent or \ public_prev_sib != prev_public_sibling: if public_prev_sib: obj.move_to(public_prev_sib, position="right") elif public_parent: # move as a first child to parent obj.move_to(public_parent, position='first-child') else: # it is a move from the right side or just save next_sibling = self.get_next_filtered_sibling(**filters) if next_sibling and next_sibling.publisher_public_id: obj.move_to(next_sibling.publisher_public, position="left") return obj def rescan_placeholders(self): """ Rescan and if necessary create placeholders in the current template. """ # inline import to prevent circular imports from cms.utils.plugins import get_placeholders placeholders = get_placeholders(self.get_template()) found = {} for placeholder in self.placeholders.all(): if placeholder.slot in placeholders: found[placeholder.slot] = placeholder for placeholder_name in placeholders: if not placeholder_name in found: placeholder = Placeholder.objects.create(slot=placeholder_name) self.placeholders.add(placeholder) found[placeholder_name] = placeholder return found