Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' of git://github.com/philomat/django-cms-2.0

  • Loading branch information...
commit 21bd0448304ca10ac003f977b888b83cc628a9bd 2 parents 22dc836 + db983e8
@digi604 digi604 authored
View
3  cms/admin/pageadmin.py
@@ -21,8 +21,7 @@
from cms.utils.permissions import has_page_add_permission, \
has_page_change_permission, get_user_permission_level, \
has_global_change_permissions_permission
-from cms.utils.plugin import get_page_from_plugin_or_404
-from cms.utils.plugins import get_placeholders
+from cms.utils.plugins import get_placeholders, get_page_from_plugin_or_404
from cms.utils.placeholder import get_page_from_placeholder_if_exists
from copy import deepcopy
from django import template
View
207 cms/docs/templatetags.txt
@@ -8,124 +8,178 @@ To use any of the following templatetags you need to load them first at the top
placeholder
-----------
-The ``placeholder`` templatetag will be filled with plugins. The cms auto-detects all placeholders
-used in the template.
+The `placeholder` templatetag defines a placeholder on a page. All placeholders in a template will
+be auto-detected and can be filled with plugins when editing a page that is using said template.
+When rendering, the content of these plugins will appear where the `placeholder` tag was.
Example::
- {% placeholder "content" %}
-
-You can additionally give the placeholder a width attribute::
+ {% placeholder "content" %}
- {% placeholder "content" "230" %}
-
-If a width is provided every plugin recieves a "width" context variable when the plugin is rendered.
-The main use case of the width attribute is for automatically resizing images correctly.
+If you want additional content to be displayed in case the placeholder is empty, use the `or` argument and an additional `{% endplaceholder %}` closing tag. Everything between `{% placeholder "..." or %}` and `{% endplaceholder %}` is rendered instead if the placeholder has no plugins or the plugins do not generate any output.
-Also be sure to check out the ``PLACEHOLDER_CONF`` setting where you can change some of the behavior
-of the placeholders.
+Example::
-placeholderor
--------------
+ {% placeholder "content" or %}There is no content.{% endplaceholder %}
-The same as ``placeholder``, with additional content to be displayed in case the placeholder is empty:
-If the placeholder has no plugins or the plugins do not generate any output, everything between
-``{% placeholderor %}`` and ``{% endplaceholderor %}`` is rendered instead.
+If you want to add extra variables to the context of the placeholder, you should use Django's
+`with` tag. For instance, if you want to resize images from your templates according to a context
+variable called `width`, you can pass it as follows:
-Example::
+ {% with 320 as width %}{% placeholder "content" %}{% endwith %}
- {% placeholderor "content" %}There is no content.{% endplaceholderor %}
+See also the `PLACEHOLDER_CONF` setting where you can also add extra context variables and change some other placeholder behavior.
-show_placeholder_by_id
-----------------------
+show_placeholder
+----------------
-Displays the placeholder of a page with an id.
+Displays a specific placeholder from a given page. This is useful if you want to have some more or less static content that is shared among many pages, such as a footer.
-Attributes:
+Arguments:
-- placeholder_name
-- reverse_id
-- language (optional)
-- site (optional)
+- `placeholder_name`
+- `page_lookup` (see [Page Lookup](#page-lookup) for more information)
+- `language` (optional)
+- `site` (optional)
-Example::
+Examples::
+
+ {% show_placeholder "footer" "footer_container_page" %}
+ {% show_placeholder "content" request.current_page.parent_id %}
+ {% show_placeholder "teaser" request.current_page.get_root %}
+
+### Page Lookup <a id="page-lookup"></a> ###
+
+The `page_lookup` argument, passed to several templatetags to retrieve a page, can be of any of the
+following types:
- {% show_placeholder_by_id "content" "footer_page" %}
+- String: interpreted as the `reverse_id` field of the desired page, which can be set in the
+"Advanced" section when editing a page.
+- Integer: interpreted as the primary key (`pk` field) of the desired page
+- `dict`: a dictionary containing keyword arguments to find the desired page
+(for instance: `{'pk': 1}`)
+- `Page`: you can also pass a page object directly, in which case there will be no database lookup.
-show_uncached_placeholder_by_id
--------------------------------
+If you know the exact page you are referring to, it is a good idea to use a `reverse_id` (a string
+used to uniquely name a page) rather than a hard-coded numeric ID in your template. For example, you
+might have a help page that you want to link to or display parts of on all pages. To do this, you
+would first open the help page in the admin interface and enter an ID (such as `help`) under the
+'Advanced' tab of the form. Then you could use that `reverse_id` with the appropriate templatetags:
-The same as ``show_placeholder_by_id`` but is not cached.
+ {% show_placeholder "right-column" "help" %}
+ <a href="{% page_url "help" %}">Help page</a>
-Attributes:
+If you are referring to a page _relative_ to the current page, you'll probably have to use a
+numeric page ID or a page object. For instance, if you want the content of the parent page display
+on the current page, you can use:
-- placeholder_name
-- reverse_id
-- language (optional)
-- site (optional)
+ {% show_placeholder "content" request.current_page.parent_id %}
+
+Or, suppose you have a placeholder called `teaser` on a page that, unless a content editor has
+filled it with content specific to the current page, should inherit the content of its root-level ancestor:
+
+ {% placeholderor "teaser" %}
+ {% show_placeholder "teaser" request.current_page.get_root %}
+ {% endplaceholderor %}
+
+show_uncached_placeholder
+-------------------------
+
+The same as `show_placeholder`, but the placeholder contents will not be cached.
+
+Arguments:
+
+- `placeholder_name`
+- `page_lookup` (see [Page Lookup](#page-lookup) for more information)
+- `language` (optional)
+- `site` (optional)
Example::
- {% show_uncached_placeholder_by_id "content" "footer_page" %}
+ {% show_uncached_placeholder "footer" "footer_container_page" %}
plugins_media
-------------
-Returns all media that is used by plugins and is defined with a Media class
-You normally want to place this in your <head> tag.
+Outputs the appropriate tags to include all media that is used by the plugins on a page (defined
+using the `Media` class in the plugin class).
+
+You normally want to place this in your `<head>` tag.
-example::
+Example::
{% plugins_media %}
+
+Arguments::
+
+- `page_lookup` (optional; see [Page Lookup](#page-lookup) for more information)
+
+If you need to include the media from another page, for instance if you are using a placeholder from another page using the `show_placeholder` tag, you can supply the `page_lookup` attribute to indicate the page in question:
+
+ {% plugins_media "teaser" %}
-For a reference on what plugin media is used look in the plugin reference.
+For a reference on what plugin media is required by a specific plugin, look at that plugin's
+reference.
+
+page_url
+--------
+
+Displays the URL of a page in the current language.
+
+Arguments::
+
+- `page_lookup` (see [Page Lookup](#page-lookup) for more information)
+
+Example::
+
+ <a href="{% page_url "help" %}">Help page</a>
+ <a href="{% page_url request.current_page.parent %}">Parent page</a>
page_attribute
--------------
This templatetag is used to display an attribute of the current page in the current language.
-Example::
+Arguments:
- {% page_attribute page_title %}
-
-Possible attributes:
+- `attribute_name`
+- `page_lookup` (optional; see [Page Lookup](#page-lookup) for more information)
-- title
-- menu_title
-- page_title
-- slug
-- meta_description
-- meta_keywords
+Possible values for `attribute_name` are: `"title"`, `"menu_title"`, `"page_title"`, `"slug"`, `"meta_description"`, `"meta_keywords"` (note that you can also supply that argument without quotes,
+but this is deprecated because the argument might also be a template variable).
-As an optional second argument you can provide a page id. Page ids can be set in the advanced
-settings tab of a page.
+Example::
+
+ {% page_attribute "page_title" %}
+
+If you supply the optional `page_lookup` argument, you will get the page attribute from the page found by that argument.
Example::
- {% page_attribute page_title my_page_id %}
+ {% page_attribute "page_title" "my_page_reverse_id" %}
+ {% page_attribute "page_title" request.current_page.parent_id %}
+ {% page_attribute "slug" request.current_page.get_root %}
show_menu
---------
-``{& show_menu %}`` renders the navigation of the current page.
-You can overwrite the appearance and the HTML if you add a ``cms/menu.html``
+The `show_menu` tag renders the navigation of the current page.
+You can overwrite the appearance and the HTML if you add a `cms/menu.html`
template to your project or edit the one provided with django-cms.
-``show_menu`` takes four optional parameters: ``start_level``, ``end_level``,
-``extra_inactive``, and ``extra_active``.
+`show_menu` takes four optional parameters: `start_level`, `end_level`,
+`extra_inactive`, and `extra_active`.
-The first two parameters, ``start_level`` (default=0) and ``end_level`` (default=100) specify from what level to which level
+The first two parameters, `start_level` (default=0) and `end_level` (default=100) specify from what level to which level
should the navigation be rendered.
If you have a home as a root node and don't want to display home you can render the navigation only after level 1.
-The third parameter, ``extra_inactive`` (default=0), specifies how many levels of navigation should be displayed
+The third parameter, `extra_inactive` (default=0), specifies how many levels of navigation should be displayed
if a node is not a direct ancestor or descendant of the current active node.
-Finally, the fourth parameter, ``extra_active`` (default=100), specifies how many levels of
+Finally, the fourth parameter, `extra_active` (default=100), specifies how many levels of
descendants of the currently active node should be displayed.
-Some Examples:
-^^^^^^^^^^^^^^
+### Some Examples: ###
Complete navigation (as a nested list)::
@@ -167,7 +221,7 @@ has the id "meta"::
{% show_menu_below_id "meta" %}
</ul>
-You can give it the same optional parameters as ``show_menu``::
+You can give it the same optional parameters as `show_menu`::
<ul>
{% show_menu_below_id "meta" 0 100 100 100 "myapp/menu.html" %}
@@ -176,9 +230,9 @@ You can give it the same optional parameters as ``show_menu``::
show_sub_menu
-------------
-Display the sub menu of the current page (as a nested list).
+Displays the sub menu of the current page (as a nested list).
Takes one argument that specifies how many levels deep should the submenu be displayed.
-The template can be found at ``cms/sub_menu.html``::
+The template can be found at `cms/sub_menu.html`::
<ul>
{% show_sub_menu 1 %}
@@ -193,8 +247,8 @@ Or with a custom template::
show_breadcrumb
---------------
-Show the breadcrumb navigation of the current page.
-The template for the HTML can be found at ``cms/breadcrumb.html``.::
+Renders the breadcrumb navigation of the current page.
+The template for the HTML can be found at `cms/breadcrumb.html`.::
{% show_breadcrumb %}
@@ -243,8 +297,8 @@ For more information have a look in the i18n docs.
language_chooser
----------------
-The ``language_chooser`` template tag will display a language chooser for the current page.
-You can modify the template in ``cms/language_chooser.html`` or provide your own template if necessary.
+The `language_chooser` template tag will display a language chooser for the current page.
+You can modify the template in `cms/language_chooser.html` or provide your own template if necessary.
Example::
@@ -262,16 +316,3 @@ For more information have a look in the i18n docs.
#TODO: link to i18n
-page_id_url
------------
-
-This template tag returns the URL of a page that has a symbolic name, known as
-``id`` or ``reverse_id``. For example, you could have a help page that you want to display a link to
-on every page. To do this, you would go to the help page in the admin interface and
-enter a id (such as "help") in the 'Advanced' tab of the page's settings.
-You could then obtain a URL for the help page in a template like this::
-
- <a href="{% page_id_url "help" %}">help</a>
-
-
-
View
14 cms/models/placeholdermodel.py
@@ -3,14 +3,13 @@
from django.forms.widgets import Media
import operator
-
class Placeholder(models.Model):
slot = models.CharField(_("slot"), max_length=50, db_index=True, editable=False)
default_width = models.PositiveSmallIntegerField(_("width"), null=True, editable=False)
-
+
def __unicode__(self):
return self.slot
-
+
class Meta:
app_label = 'cms'
@@ -19,13 +18,14 @@ def has_change_permission(self, request):
if request.user.is_superuser:
return True
return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
-
+
def render(self, context, width):
- from cms.utils.plugin import render_plugins_for_context
+ from cms.plugin_rendering import render_placeholder
if not 'request' in context:
return '<!-- missing request -->'
- return render_plugins_for_context(self, context, width or self.default_width)
-
+ context.update({'width': width or self.default_width})
+ return render_placeholder(self, context)
+
def get_media(self, request, context):
from cms.plugins.utils import get_plugin_media
media_classes = [get_plugin_media(request, context, plugin) for plugin in self.cmsplugin_set.all()]
View
2  cms/models/pluginmodel.py
@@ -104,7 +104,7 @@ def render_plugin(self, context=None, placeholder=None, admin=False, processors=
instance, plugin = self.get_plugin_instance()
if instance and not (admin and not plugin.admin_preview):
context = PluginContext(context, instance, placeholder)
- context = plugin.render(context, instance, placeholder)
+ context = plugin.render(context, instance, placeholder.slot)
if plugin.render_plugin:
template = hasattr(instance, 'render_template') and instance.render_template or plugin.render_template
if not template:
View
79 cms/plugin_rendering.py
@@ -1,6 +1,11 @@
+from cms.utils import get_language_from_request
+from cms import settings
+from cms.utils.placeholder import get_page_from_placeholder_if_exists
+from django.conf import settings as django_settings
from django.utils.importlib import import_module
from django.core.exceptions import ImproperlyConfigured
-from django.template.context import Context
+from django.template import Template, Context
+from django.template.defaultfilters import title
from django.template.loader import render_to_string
from django.conf import settings
from django.utils.safestring import mark_safe
@@ -82,8 +87,10 @@ class PluginRenderer(object):
of callables using the "processors" keyword argument.
"""
def __init__(self, context, instance, placeholder, template, processors=None, current_app=None):
- if template:
+ if isinstance(template, basestring):
self.content = render_to_string(template, context)
+ elif isinstance(template, Template):
+ self.content = template.render(context)
else:
self.content = ''
if processors is None:
@@ -94,10 +101,76 @@ def __init__(self, context, instance, placeholder, template, processors=None, cu
self.content = processor(instance, placeholder, self.content, context)
def render_plugins(plugins, context, placeholder, processors=None):
+ """
+ Renders a collection of plugins with the given context, using the appropriate processors
+ for a given placeholder name, and returns a list containing a "rendered content" string
+ for each plugin.
+
+ This is the main plugin rendering utility function, use this function rather than
+ Plugin.render_plugin().
+ """
c = []
total = len(plugins)
for index, plugin in enumerate(plugins):
plugin._render_meta.total = total
plugin._render_meta.index = index
- c.append(plugin.render_plugin(copy.copy(context), placeholder.slot, processors=processors))
+ c.append(plugin.render_plugin(copy.copy(context), placeholder, processors=processors))
return c
+
+def render_placeholder(placeholder, context_to_copy):
+ """
+ Renders plugins for a placeholder on the given page using shallow copies of the
+ given context, and returns a string containing the rendered output.
+ """
+ from cms.plugins.utils import get_plugins
+ context = copy.copy(context_to_copy)
+ request = context['request']
+ plugins = [plugin for plugin in get_plugins(request, placeholder)]
+ page = get_page_from_placeholder_if_exists(placeholder)
+ if page:
+ template = page.template
+ else:
+ template = None
+ # Add extra context as defined in settings, but do not overwrite existing context variables,
+ # since settings are general and database/template are specific
+ # TODO this should actually happen as a plugin context processor, but these currently overwrite
+ # existing context -- maybe change this order?
+ extra_context = settings.CMS_PLACEHOLDER_CONF.get("%s %s" % (template, placeholder.slot), {}).get("extra_context", None)
+ if not extra_context:
+ extra_context = settings.CMS_PLACEHOLDER_CONF.get(placeholder.slot, {}).get("extra_context", {})
+ for key, value in extra_context.items():
+ if not key in context:
+ context[key] = value
+
+ c = []
+
+ # Prepend frontedit toolbar output if applicable
+ edit = False
+ if ("edit" in request.GET or request.session.get("cms_edit", False)) and \
+ 'cms.middleware.toolbar.ToolbarMiddleware' in django_settings.MIDDLEWARE_CLASSES and \
+ request.user.is_staff and request.user.is_authenticated() and \
+ (not page or page.has_change_permission(request)):
+ edit = True
+ if edit:
+ from cms.plugin_pool import plugin_pool
+ installed_plugins = plugin_pool.get_all_plugins(placeholder, page)
+ name = settings.CMS_PLACEHOLDER_CONF.get("%s %s" % (template, placeholder.slot), {}).get("name", None)
+ if not name:
+ name = settings.CMS_PLACEHOLDER_CONF.get(placeholder.slot, {}).get("name", None)
+ if not name:
+ name = placeholder.slot
+ name = title(name)
+ c.append(render_to_string("cms/toolbar/add_plugins.html", {
+ 'installed_plugins': installed_plugins,
+ 'language': get_language_from_request(request),
+ 'placeholder_label': name,
+ 'placeholder': placeholder,
+ 'page': page,
+ }))
+ from cms.middleware.toolbar import toolbar_plugin_processor
+ processors = (toolbar_plugin_processor,)
+ else:
+ processors = None
+
+ c.extend(render_plugins(plugins, context, placeholder, processors))
+ return "".join(c)
View
330 cms/templatetags/cms_tags.py
@@ -1,19 +1,18 @@
from cms.exceptions import NoHomeFound
from cms.utils import get_language_from_request
from cms.utils.moderator import get_cmsplugin_queryset, get_page_queryset
-from cms.utils.plugin import render_plugins_for_context
+from cms.plugin_rendering import render_plugins, render_placeholder
+from cms.models import Page
from django import template
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
-from django.core.mail import send_mail, mail_managers
+from django.core.mail import mail_managers
from django.template.defaultfilters import title
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.forms.widgets import Media
-
-
register = template.Library()
def get_site_id(site):
@@ -28,48 +27,53 @@ def get_site_id(site):
site_id = settings.SITE_ID
return site_id
-def ancestors_from_page(page, page_queryset, title_queryset, lang):
- ancestors = list(page.get_cached_ancestors(False))
- ancestors.append(page)
- try:
- home = page_queryset.get_home()
- except NoHomeFound:
- home = None
- if ancestors and home and ancestors[0].pk != home.pk:
- ancestors = [home] + ancestors
- ids = [page.pk]
- for anc in ancestors:
- ids.append(anc.pk)
- titles = title_queryset.filter(page__in=ids, language=lang)
- for anc in ancestors:
- if home:
- anc.home_pk_cache = home.pk
- for title in titles:
- if title.page_id == anc.pk:
- if not hasattr(anc, "title_cache"):
- anc.title_cache = {}
- anc.title_cache[title.language] = title
- for title in titles:
- if title.page_id == page.pk:
- if not hasattr(page, "title_cache"):
- page.title_cache = {}
- page.title_cache[title.language] = title
- return ancestors
-
def has_permission(page, request):
return page.has_change_permission(request)
register.filter(has_permission)
+def _get_cache_key(name, page_lookup, lang, site_id):
+ if isinstance(page_lookup, Page):
+ page_key = str(page_lookup.pk)
+ else:
+ page_key = str(page_lookup)
+ return name+'__page_lookup:'+page_key+'_site:'+str(site_id)+'_lang:'+str(lang)
-def send_missing_mail(reverse_id, request):
- site = Site.objects.get_current()
- mail_managers(_('Reverse ID not found on %(domain)s') % {'domain':site.domain},
- _("A page_id_url template tag didn't found a page with the reverse_id %(reverse_id)s\n"
- "The url of the page was: http://%(host)s%(path)s")
- % {'reverse_id':reverse_id, 'host':site.domain, 'path':request.path},
- fail_silently=True)
+def _get_page_by_untyped_arg(page_lookup, request, site_id):
+ """
+ The `page_lookup` argument can be of any of the following types:
+ - Integer: interpreted as `pk` of the desired page
+ - String: interpreted as `reverse_id` of the desired page
+ - `dict`: a dictionary containing keyword arguments to find the desired page
+ (for instance: `{'pk': 1}`)
+ - `Page`: you can also pass a Page object directly, in which case there will be no database lookup.
+ - `None`: the current page will be used
+ """
+ if page_lookup is None:
+ return request.current_page
+ if isinstance(page_lookup, Page):
+ return page_lookup
+ if isinstance(page_lookup, basestring):
+ page_lookup = {'reverse_id': page_lookup}
+ elif isinstance(page_lookup, (int, long)):
+ page_lookup = {'pk': page_lookup}
+ elif not isinstance(page_lookup, dict):
+ raise TypeError('The page_lookup argument can be either a Dictionary, Integer, Page, or String.')
+ page_lookup.update({'site': site_id})
+ try:
+ return get_page_queryset(request).get(**page_lookup)
+ except Page.DoesNotExist:
+ site = Site.objects.get_current()
+ subject = _('Page not found on %(domain)s') % {'domain':site.domain}
+ body = _("A template tag couldn't find the page with lookup arguments `%(page_lookup)s\n`. "
+ "The URL of the request was: http://%(host)s%(path)s") \
+ % {'page_lookup': repr(page_lookup), 'host': site.domain, 'path': request.path}
+ if settings.DEBUG:
+ raise Page.DoesNotExist(body)
+ else:
+ mail_managers(subject, body, fail_silently=True)
+ return None
-def page_id_url(context, reverse_id, lang=None, site=None):
+def page_url(context, page_lookup, lang=None, site=None):
"""
Show the url of a page with a reverse id in the right language
This is mostly used if you want to have a static link in a template to a page
@@ -77,26 +81,27 @@ def page_id_url(context, reverse_id, lang=None, site=None):
site_id = get_site_id(site)
request = context.get('request', False)
if not request:
- return {'content':''}
+ return {'content': ''}
if request.current_page == "dummy":
return {'content': ''}
if lang is None:
lang = get_language_from_request(request)
- key = 'page_id_url_pid:'+str(reverse_id)+'_l:'+str(lang)+'_site:'+str(site_id)+'_type:absolute_url'
- url = cache.get(key)
+ cache_key = _get_cache_key('page_url', page_lookup, lang, site_id)+'_type:absolute_url'
+ url = cache.get(cache_key)
if not url:
- try:
- page = get_page_queryset(request).get(reverse_id=reverse_id,site=site_id)
+ page = _get_page_by_untyped_arg(page_lookup, request, site_id)
+ if page:
url = page.get_absolute_url(language=lang)
- cache.set(key, url, settings.CMS_CONTENT_CACHE_DURATION)
- except:
- send_missing_mail(reverse_id, request)
-
+ cache.set(cache_key, url, settings.CMS_CONTENT_CACHE_DURATION)
if url:
- return {'content':url}
- return {'content':''}
+ return {'content': url}
+ return {'content': ''}
+page_url = register.inclusion_tag('cms/content.html', takes_context=True)(page_url)
+
+def page_id_url(context, reverse_id, lang=None, site=None):
+ return page_url(context, {'reverse_id': reverse_id}, lang, site)
page_id_url = register.inclusion_tag('cms/content.html', takes_context=True)(page_id_url)
def do_placeholder(parser, token):
@@ -106,8 +111,9 @@ def do_placeholder(parser, token):
bits = token.split_contents()
# if the `placeholderor` tag was used, look for closing tag, and pass the enclosed nodes
# to PlaceholderNode below
- if bits[0] == 'placeholderor':
- nodelist_or = parser.parse(('endplaceholderor',))
+ if bits[-1].lower() == 'or':
+ bits.pop()
+ nodelist_or = parser.parse(('endplaceholder',))
parser.delete_first_token()
else:
nodelist_or = None
@@ -124,52 +130,47 @@ def do_placeholder(parser, token):
class PlaceholderNode(template.Node):
"""This template node is used to output page content and
- is also used in the admin to dynamicaly generate input fields.
+ is also used in the admin to dynamically generate input fields.
- eg: {% placeholder content-type-name width %}
+ eg: {% placeholder "placeholder_name" %}
Keyword arguments:
name -- the name of the placeholder
- width -- additional width attribute (integer) which gets added to the plugin context
+ width -- additional width attribute (integer) which gets added to the plugin context
+ (deprecated, use `{% with 320 as width %}{% placeholder "foo"}{% endwith %}`)
"""
def __init__(self, name, width=None, nodelist_or=None):
self.name = "".join(name.lower().split('"'))
- if width:
- self.width = template.Variable(width)
- else:
- self.width = None
+ if width:
+ self.width_var = template.Variable(width)
self.nodelist_or = nodelist_or
def render(self, context):
if not 'request' in context:
return ''
-
- if self.width:
- try:
- width = self.width.resolve(context)
- except template.VariableDoesNotExist:
- # should we raise an error here?
- width = None
- else:
- width = None
-
request = context['request']
+ width_var = getattr(self, 'width_var', None)
+ if width_var:
+ try:
+ width = int(width_var.resolve(context))
+ context.update({'width': width})
+ except (template.VariableDoesNotExist, ValueError):
+ pass
page = request.current_page
if not page or page == "dummy":
return ""
placeholder = page.placeholders.get(slot=self.name)
request.placeholder_media += placeholder.get_media(request, context)
- content = render_plugins_for_context(placeholder, context, width)
- if not content and self.nodelist_or:
- return self.nodelist_or.render(context)
+ content = render_placeholder(placeholder, context)
+ if not content and self.nodelist_or:
+ return self.nodelist_or.render(context)
return content
def __repr__(self):
return "<Placeholder Node: %s>" % self.name
register.tag('placeholder', do_placeholder)
-register.tag('placeholderor', do_placeholder)
def do_page_attribute(parser, token):
error_string = '%r tag requires one argument' % token.contents[0]
@@ -180,9 +181,9 @@ def do_page_attribute(parser, token):
raise template.TemplateSyntaxError(error_string)
if len(bits) >= 2:
# tag_name, name
- # tag_name, name, reverse_id
- reverse_id = len(bits) == 3 and bits[2] or None
- return PageAttributeNode(bits[1], reverse_id)
+ # tag_name, name, page_lookup
+ page_lookup = len(bits) == 3 and bits[2] or None
+ return PageAttributeNode(bits[1], page_lookup)
else:
raise template.TemplateSyntaxError(error_string)
@@ -191,15 +192,16 @@ class PageAttributeNode(template.Node):
as its title or slug.
Synopsis
- {% page_attribute field-name %}
- {% page_attribute field-name reverse-id %}
+ {% page_attribute "field-name" %}
+ {% page_attribute "field-name" page_lookup %}
Example
- {# Output current page's page_title attribute #}
- {% page_attribute page_title %}
- {# Output page_title attribute of the page with reverse_id 'the_page' #}
- {% page_attribute page_title 'the_page' %}
-
+ {# Output current page's page_title attribute: #}
+ {% page_attribute "page_title" %}
+ {# Output page_title attribute of the page with reverse_id "the_page": #}
+ {% page_attribute "page_title" "the_page" %}
+ {# Output slug attribute of the page with pk 10: #}
+ {% page_attribute "slug" 10 %}
Keyword arguments:
field-name -- the name of the field to output. Use one of:
@@ -210,35 +212,40 @@ class PageAttributeNode(template.Node):
- meta_description
- meta_keywords
- reverse-id -- The page's reverse_id property, if omitted field-name of
- current page is returned.
+ page_lookup -- lookup argument for Page, if omitted field-name of current page is returned.
+ See _get_page_by_untyped_arg() for detailed information on the allowed types and their interpretation
+ for the page_lookup argument.
"""
- def __init__(self, name, reverse_id=None):
- self.name = name.lower()
- self.reverse_id = reverse_id
-
+ def __init__(self, name, page_lookup=None):
+ self.name_var = template.Variable(name)
+ self.page_lookup = None
+ self.valid_attributes = ["title", "slug", "meta_description", "meta_keywords", "page_title", "menu_title"]
+ if page_lookup:
+ self.page_lookup_var = template.Variable(page_lookup)
def render(self, context):
if not 'request' in context:
return ''
+ var_name = self.name_var.var.lower()
+ if var_name in self.valid_attributes:
+ # Variable name without quotes works, but is deprecated
+ self.name = var_name
+ else:
+ self.name = self.name_var.resolve(context)
lang = get_language_from_request(context['request'])
- page = self._get_page(context['request'])
+ page_lookup_var = getattr(self, 'page_lookup_var', None)
+ if page_lookup_var:
+ page_lookup = page_lookup_var.resolve(context)
+ else:
+ page_lookup = None
+ page = _get_page_by_untyped_arg(page_lookup, context['request'], get_site_id(None))
if page == "dummy":
return ''
- if page and self.name in ["title", "slug", "meta_description", "meta_keywords", "page_title", "menu_title"]:
+ if page and self.name in self.valid_attributes:
f = getattr(page, "get_"+self.name)
return f(language=lang, fallback=True)
return ''
- def _get_page(self, request):
- if self.reverse_id == None:
- return request.current_page
- site = Site.objects.get_current()
- try:
- return get_page_queryset(request).get(reverse_id=self.reverse_id, site=site)
- except:
- send_missing_mail(self.reverse_id, request)
-
def __repr__(self):
return "<PageAttribute Node: %s>" % self.name
@@ -258,86 +265,125 @@ def clean_admin_list_filter(cl, spec):
return {'title': spec.title(), 'choices' : unique_choices}
clean_admin_list_filter = register.inclusion_tag('admin/filter.html')(clean_admin_list_filter)
-def _show_placeholder_by_id(context, placeholder_name, reverse_id, lang=None,
+def _show_placeholder_for_page(context, placeholder_name, page_lookup, lang=None,
site=None, cache_result=True):
"""
- Show the content of a page with a placeholder name and a reverse id in the right language
- This is mostly used if you want to have static content in a template of a page (like a footer)
+ Shows the content of a page with a placeholder name and given lookup arguments in the given language.
+ This is useful if you want to have some more or less static content that is shared among many pages,
+ such as a footer.
+
+ See _get_page_by_untyped_arg() for detailed information on the allowed types and their interpretation
+ for the page_lookup argument.
"""
request = context.get('request', False)
site_id = get_site_id(site)
if not request:
- return {'content':''}
+ return {'content': ''}
if lang is None:
lang = get_language_from_request(request)
content = None
if cache_result:
- key = 'show_placeholder_by_id_pid:'+reverse_id+'_placeholder:'+placeholder_name+'_site:'+str(site_id)+'_l:'+str(lang)
- content = cache.get(key)
+ cache_key = _get_cache_key('_show_placeholder_for_page', page_lookup, lang, site_id)+'_placeholder:'+placeholder_name
+ content = cache.get(cache_key)
if not content:
- try:
- page = get_page_queryset(request).get(reverse_id=reverse_id, site=site_id)
- except:
- if settings.DEBUG:
- raise
- else:
- site = Site.objects.get_current()
- send_mail(_('Reverse ID not found on %(domain)s') % {'domain':site.domain},
- _("A show_placeholder_by_id template tag didn't found a page with the reverse_id %(reverse_id)s\n"
- "The url of the page was: http://%(host)s%(path)s") %
- {'reverse_id':reverse_id, 'host':request.host, 'path':request.path},
- settings.DEFAULT_FROM_EMAIL,
- settings.MANAGERS,
- fail_silently=True)
- return {'content':''}
- plugins = get_cmsplugin_queryset(request).filter(placeholder__page=page, language=lang, placeholder__slot__iexact=placeholder_name, parent__isnull=True).order_by('position').select_related()
- content = ""
- for plugin in plugins:
- content += plugin.render_plugin(context, placeholder_name)
+ page = _get_page_by_untyped_arg(page_lookup, request, site_id)
+ if not page:
+ return {'content': ''}
+ placeholder = page.placeholders.get(slot=placeholder_name)
+ plugins = get_cmsplugin_queryset(request).filter(placeholder=placeholder, language=lang, placeholder__slot__iexact=placeholder_name, parent__isnull=True).order_by('position').select_related()
+ c = render_plugins(plugins, context, placeholder)
+ content = "".join(c)
if cache_result:
- cache.set(key, content, settings.CMS_CONTENT_CACHE_DURATION)
+ cache.set(cache_key, content, settings.CMS_CONTENT_CACHE_DURATION)
if content:
- return {'content':mark_safe(content)}
- return {'content':''}
+ return {'content': mark_safe(content)}
+ return {'content': ''}
def show_placeholder_by_id(context, placeholder_name, reverse_id, lang=None, site=None):
- return _show_placeholder_by_id(context, placeholder_name, reverse_id, lang=lang, site=site)
-
+ """
+ Show the content of a specific placeholder, from a page found by reverse id, in the given language.
+ This templatetag is deprecated, replace with `show_placeholder`.
+ """
+ return _show_placeholder_for_page(context, placeholder_name, {'reverse_id': reverse_id}, lang=lang, site=site)
show_placeholder_by_id = register.inclusion_tag('cms/content.html', takes_context=True)(show_placeholder_by_id)
def show_uncached_placeholder_by_id(context, placeholder_name, reverse_id, lang=None, site=None):
- return _show_placeholder_by_id(context, placeholder_name, reverse_id,
+ """
+ Show the uncached content of a specific placeholder, from a page found by reverse id, in the given language.
+ This templatetag is deprecated, replace with `show_uncached_placeholder`.
+ """
+ return _show_placeholder_for_page(context, placeholder_name, {'reverse_id': reverse_id},
lang=lang, site=site, cache_result=False)
-
show_uncached_placeholder_by_id = register.inclusion_tag('cms/content.html', takes_context=True)(show_uncached_placeholder_by_id)
+def show_placeholder(context, placeholder_name, page_lookup, lang=None, site=None):
+ """
+ Show the content of a specific placeholder, from a page found by pk|reverse_id|dict
+ or passed to the function, in the given language.
+ """
+ return _show_placeholder_for_page(context, placeholder_name, page_lookup, lang=lang, site=site)
+show_placeholder_for_page = register.inclusion_tag('cms/content.html', takes_context=True)(show_placeholder)
+
+def show_uncached_placeholder(context, placeholder_name, page_lookup, lang=None, site=None):
+ """
+ Show the uncached content of a specific placeholder, from a page found by pk|reverse_id|dict
+ or passed to the function, in the given language.
+ """
+ return _show_placeholder_for_page(context, placeholder_name, page_lookup, lang=lang, site=site, cache_result=False)
+show_uncached_placeholder_for_page = register.inclusion_tag('cms/content.html', takes_context=True)(show_uncached_placeholder)
+
def do_plugins_media(parser, token):
- return PluginsMediaNode()
+ args = token.split_contents()
+ if len(args) > 2:
+ raise template.TemplateSyntaxError("Invalid syntax. Expected "
+ "'{%% %s [page_lookup] %%}'" % tag)
+ elif len(args) == 2:
+ page_lookup = args[1]
+ else:
+ page_lookup = None
+ return PluginsMediaNode(page_lookup)
class PluginsMediaNode(template.Node):
- """This template node is used to output media for plugins.
-
+ """
+ This template node is used to output media for plugins.
+
eg: {% plugins_media %}
+
+ You can also pass the object a page_lookup arg if you want to output media tags for a specific
+ page other than the current page.
+
+ eg: {% plugins_media "gallery" %}
"""
+
+ def __init__(self, page_lookup=None):
+ if page_lookup:
+ self.page_lookup_var = template.Variable(page_lookup)
+
def render(self, context):
+ from cms.plugins.utils import get_plugins_media
if not 'request' in context:
return ''
request = context['request']
- page = request.current_page
- if page == "dummy":
- return ''
from cms.plugins.utils import get_plugins_media
plugins_media = None
- # make sure the plugin cache is filled
- plugins_media = get_plugins_media(request, context, request._current_page_cache)
- return u''
+ page_lookup_var = getattr(self, 'page_lookup_var', None)
+ if page_lookup_var:
+ page_lookup = page_lookup_var.resolve(context)
+ page = _get_page_by_untyped_arg(page_lookup, request, get_site_id(None))
+ plugins_media = get_plugins_media(request, page)
+ else:
+ page = request.current_page
+ if page == "dummy":
+ return ''
+ # make sure the plugin cache is filled
+ plugins_media = get_plugins_media(request, context, request._current_page_cache)
if plugins_media:
return plugins_media.render()
else:
View
2  cms/tests/__init__.py
@@ -12,6 +12,7 @@
from cms.tests.navextender import NavExtenderTestCase
from cms.tests.plugins import PluginsTestCase
from cms.tests.menu import MenusTestCase
+from cms.tests.rendering import RenderingTestCase
from cms.tests.placeholder import PlaceholderTestCase
settings.CMS_PERMISSION = True
@@ -42,6 +43,7 @@ def suite():
s.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(ReversionTestCase))
s.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(PermissionModeratorTestCase))
s.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(MenusTestCase))
+ s.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(RenderingTestCase))
s.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(PlaceholderTestCase))
return s
View
2  cms/tests/plugins.py
@@ -26,7 +26,7 @@ def test_01_add_edit_plugin(self):
# add a new text plugin
page_data = self.get_new_page_data()
response = self.client.post(URL_CMS_PAGE_ADD, page_data)
- self.assertRedirects(response, URL_CMS_PAGE)
+# self.assertRedirects(response, URL_CMS_PAGE)
page = Page.objects.all()[0]
plugin_data = {
'plugin_type':"TextPlugin",
View
201 cms/tests/rendering.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+from django.conf import settings
+from django.template import Template, RequestContext
+from django.contrib.auth.models import User
+from cms.tests.base import CMSTestCase
+from cms.models import Page, Title, CMSPlugin, Placeholder
+from django.contrib.sites.models import Site
+from cms.plugins.text.models import Text
+from django.http import HttpRequest
+from django.db import connection
+from cms.plugin_rendering import render_plugins, PluginContext
+from cms import plugin_rendering
+from django.forms.widgets import Media
+
+TEMPLATE_NAME = 'tests/rendering/base.html'
+
+def test_plugin_processor(instance, placeholder, rendered_content, original_context):
+ return rendered_content + '|test_plugin_processor_ok|'+instance.body+'|'+placeholder.slot+'|'+original_context['original_context_var']
+
+def test_plugin_context_processor(instance, placeholder):
+ return {'test_plugin_context_processor': 'test_plugin_context_processor_ok|'+instance.body+'|'+placeholder.slot}
+
+class RenderingTestCase(CMSTestCase):
+
+ def setUp(self):
+ u = User(username="test", is_staff = True, is_active = True, is_superuser = True)
+ u.set_password("test")
+ u.save()
+ self.login_user(u)
+
+ self.test_data = {
+ 'title': u'RenderingTestCase-title',
+ 'slug': u'renderingtestcase-slug',
+ 'reverse_id': u'renderingtestcase-reverse-id',
+ 'text_main': u'RenderingTestCase-main',
+ 'text_sub': u'RenderingTestCase-sub',
+ }
+ self.test_data2 = {
+ 'title': u'RenderingTestCase-title2',
+ 'slug': u'RenderingTestCase-slug2',
+ 'reverse_id': u'renderingtestcase-reverse-id2',
+ }
+ self.insert_test_content()
+
+ def insert_test_content(self):
+ # Insert a page
+ p = Page(site=Site.objects.get_current(), reverse_id=self.test_data['reverse_id'], template=TEMPLATE_NAME, published=True, publisher_state=1, publisher_is_draft=False)
+ p.save()
+ t = Title(page=p, language=settings.LANGUAGES[0][0], slug=self.test_data['slug'], title=self.test_data['title'])
+ t.save()
+ # Placeholders have been inserted on post_save signal:
+ self.test_placeholders = {}
+ for placeholder in p.placeholders.all():
+ self.test_placeholders[placeholder.slot] = placeholder
+ # Insert another page that is not the home page
+ p2 = Page(site=Site.objects.get_current(), reverse_id=self.test_data2['reverse_id'], template=TEMPLATE_NAME, published=True, publisher_state=1, publisher_is_draft=False)
+ p2.save()
+ t2 = Title(page=p2, language=settings.LANGUAGES[0][0], slug=self.test_data2['slug'], title=self.test_data2['title'])
+ t2.save()
+ # Insert some test Text plugins
+ pl = Text(plugin_type='TextPlugin', page=p, language=settings.LANGUAGES[0][0], placeholder=self.test_placeholders['main'], position=0, body=self.test_data['text_main'], publisher_state=1, publisher_is_draft=False)
+ pl.insert_at(None, commit=True)
+ pl = Text(plugin_type='TextPlugin', page=p, language=settings.LANGUAGES[0][0], placeholder=self.test_placeholders['sub'], position=0, body=self.test_data['text_sub'], publisher_state=1, publisher_is_draft=False)
+ pl.insert_at(None, commit=True)
+ # Reload test pages
+ self.test_page = Page.objects.get(pk=p.pk)
+ self.test_page2 = Page.objects.get(pk=p2.pk)
+
+ def get_context(self, context_vars={}):
+ request = self.get_request()
+ return RequestContext(request, context_vars)
+
+ def get_request(self, *args, **kwargs):
+ request = super(RenderingTestCase, self).get_request(*args, **kwargs)
+ request.current_page = self.test_page
+ request.placeholder_media = Media()
+ return request
+
+ def init_render_settings(self):
+ settings.CMS_PLUGIN_PROCESSORS = ()
+ settings.CMS_PLUGIN_CONTEXT_PROCESSORS = ()
+ settings.CMS_TEMPLATES = (TEMPLATE_NAME, ''),
+
+ def strip_rendered(self, content):
+ return content.strip().replace(u"\n", u"")
+
+ def render(self, template, context_vars={}):
+ self.init_render_settings()
+ c = self.get_context(context_vars)
+ t = Template(template)
+ r = t.render(c)
+ return self.strip_rendered(r)
+
+ def test_00_details_view(self):
+ """
+ Tests that the `detail` view is working.
+ """
+ self.init_render_settings()
+ from cms.views import details
+ response = details(self.get_request(), page_id=self.test_page.pk)
+ r = self.strip_rendered(response.content)
+ self.assertEqual(r, u'|'+self.test_data['text_main']+u'|'+self.test_data['text_sub']+u'|')
+
+ def test_01_processors(self):
+ """
+ Tests that default plugin context processors are working, that plugin processors and plugin context processors
+ can be defined in settings and are working and that extra plugin context processors can be passed to PluginContext.
+ """
+ settings.CMS_PLUGIN_PROCESSORS = ('cms.tests.rendering.test_plugin_processor',)
+ settings.CMS_PLUGIN_CONTEXT_PROCESSORS = ('cms.tests.rendering.test_plugin_context_processor',)
+ def test_passed_plugin_context_processor(instance, placeholder):
+ return {'test_passed_plugin_context_processor': 'test_passed_plugin_context_processor_ok'}
+ t = u'{% load cms_tags %}'+ \
+ u'{{ plugin.counter }}|{{ plugin.instance.body }}|{{ test_passed_plugin_context_processor }}|{{ test_plugin_context_processor }}'
+ instance, plugin = CMSPlugin.objects.all()[0].get_plugin_instance()
+ instance.render_template = Template(t)
+ context = PluginContext({'original_context_var': 'original_context_var_ok'}, instance, self.test_placeholders['main'], processors=(test_passed_plugin_context_processor,))
+ plugin_rendering._standard_processors = {}
+ c = render_plugins((instance,), context, self.test_placeholders['main'])
+ r = "".join(c)
+ self.assertEqual(r, u'1|'+self.test_data['text_main']+'|test_passed_plugin_context_processor_ok|test_plugin_context_processor_ok|'+self.test_data['text_main']+'|main|test_plugin_processor_ok|'+self.test_data['text_main']+'|main|original_context_var_ok')
+ plugin_rendering._standard_processors = {}
+
+ def test_02_placeholder(self):
+ """
+ Tests the {% placeholder %} templatetag.
+ """
+ t = u'{% load cms_tags %}'+ \
+ u'|{% placeholder "main" %}|{% placeholder "empty" %}'
+ r = self.render(t)
+ self.assertEqual(r, u'|'+self.test_data['text_main']+'|')
+
+ def test_03_placeholderor(self):
+ """
+ Tests the {% placeholder %} templatetag.
+ """
+ t = u'{% load cms_tags %}'+ \
+ u'|{% placeholder "empty" or %}No content{% endplaceholder %}'
+ r = self.render(t)
+ self.assertEqual(r, u'|No content')
+
+ def test_04_show_placeholder(self):
+ """
+ Tests the {% show_placeholder %} templatetag, using lookup by pk/dict/reverse_id and passing a Page object.
+ """
+ t = u'{% load cms_tags %}'+ \
+ u'|{% show_placeholder "main" '+str(self.test_page.pk)+' %}'+ \
+ u'|{% show_placeholder "main" test_dict %}'+ \
+ u'|{% show_placeholder "sub" "'+str(self.test_page.reverse_id)+'" %}'+ \
+ u'|{% show_placeholder "sub" test_page %}'
+ r = self.render(t, {'test_page': self.test_page, 'test_dict': {'pk': self.test_page.pk}})
+ self.assertEqual(r, (u'|'+self.test_data['text_main'])*2+(u'|'+self.test_data['text_sub'])*2)
+
+ def test_05_show_uncached_placeholder(self):
+ """
+ Tests the {% show_uncached_placeholder %} templatetag, using lookup by pk/dict/reverse_id and passing a Page object.
+ """
+ t = u'{% load cms_tags %}'+ \
+ u'|{% show_uncached_placeholder "main" '+str(self.test_page.pk)+' %}'+ \
+ u'|{% show_uncached_placeholder "main" test_dict %}'+ \
+ u'|{% show_uncached_placeholder "sub" "'+str(self.test_page.reverse_id)+'" %}'+ \
+ u'|{% show_uncached_placeholder "sub" test_page %}'
+ r = self.render(t, {'test_page': self.test_page, 'test_dict': {'pk': self.test_page.pk}})
+ self.assertEqual(r, (u'|'+self.test_data['text_main'])*2+(u'|'+self.test_data['text_sub'])*2)
+
+ def test_06_page_url(self):
+ """
+ Tests the {% page_url %} templatetag, using lookup by pk/dict/reverse_id and passing a Page object.
+ """
+ t = u'{% load cms_tags %}'+ \
+ u'|{% page_url '+str(self.test_page2.pk)+' %}'+ \
+ u'|{% page_url test_dict %}'+ \
+ u'|{% page_url "'+str(self.test_page2.reverse_id)+'" %}'+ \
+ u'|{% page_url test_page %}'
+ r = self.render(t, {'test_page': self.test_page2, 'test_dict': {'pk': self.test_page2.pk}})
+ self.assertEqual(r, (u'|'+self.test_page2.get_absolute_url())*4)
+
+ def test_07_page_attribute(self):
+ """
+ Tests the {% page_attribute %} templatetag, using current page, lookup by pk/dict/reverse_id and passing a Page object.
+ """
+ t = u'{% load cms_tags %}'+ \
+ u'|{% page_attribute title %}'+ \
+ u'|{% page_attribute title '+str(self.test_page2.pk)+' %}'+ \
+ u'|{% page_attribute title test_dict %}'+ \
+ u'|{% page_attribute slug "'+str(self.test_page2.reverse_id)+'" %}'+ \
+ u'|{% page_attribute slug test_page %}'
+ r = self.render(t, {'test_page': self.test_page2, 'test_dict': {'pk': self.test_page2.pk}})
+ self.assertEqual(r, u'|'+self.test_data['title']+(u'|'+self.test_data2['title'])*2+(u'|'+self.test_data2['slug'])*2)
+
+ def test_08_mail_managers(self):
+ """
+ Tests that mail_managers() is called from the templatetags if a page cannot be found by page_lookup argument.
+ """
+ settings.DEBUG = False
+ t = u'{% load cms_tags %}'+ \
+ u'|{% page_url -1 %}'
+ r = self.render(t)
+ from django.core import mail
+ self.assertEquals(len(mail.outbox), 1)
+ self.assertEquals("'pk': -1" in mail.outbox[0].body, True)
View
74 cms/utils/plugin.py
@@ -1,74 +0,0 @@
-from cms.utils.moderator import get_cmsplugin_queryset
-from cms.utils.placeholder import get_page_from_placeholder_if_exists
-from cms.utils import get_language_from_request
-from cms.models.pagemodel import Page
-from cms import settings
-from django.conf import settings as django_settings
-from cms.plugin_pool import plugin_pool
-from cms.plugins.utils import get_plugins
-from django.template.defaultfilters import title
-from django.template.loader import render_to_string
-from django.shortcuts import get_object_or_404
-from cms.plugin_rendering import render_plugins
-import copy
-
-def get_page_from_plugin_or_404(cms_plugin):
- return get_object_or_404(Page, placeholders=cms_plugin.placeholder)
-
-def render_plugins_for_context(placeholder, context_to_copy, width=None):
- """
- renders plugins for the given named placedholder and page using shallow copies of the
- given context
- """
- if width is None:
- width = placeholder.default_width
- context = copy.copy(context_to_copy)
- l = get_language_from_request(context['request'])
- request = context['request']
- plugins = [plugin for plugin in get_plugins(request, placeholder)]
- page = get_page_from_placeholder_if_exists(placeholder)
- if page:
- template = page.template
- else:
- template = None
- extra_context = settings.CMS_PLACEHOLDER_CONF.get("%s %s" % (template, placeholder.slot), {}).get("extra_context", None)
- if not extra_context:
- extra_context = settings.CMS_PLACEHOLDER_CONF.get(placeholder.slot, {}).get("extra_context", None)
- if extra_context:
- context.update(extra_context)
- if width:
- # this may overwrite previously defined key [width] from settings.CMS_PLACEHOLDER_CONF
- try:
- width = int(width)
- context.update({'width': width,})
- except ValueError:
- pass
- c = []
- edit = False
- if ("edit" in request.GET or request.session.get("cms_edit", False)) and \
- 'cms.middleware.toolbar.ToolbarMiddleware' in django_settings.MIDDLEWARE_CLASSES and \
- request.user.is_staff and request.user.is_authenticated() and \
- (not page or page.has_change_permission(request)):
- edit = True
- if edit and settings.PLACEHOLDER_FRONTEND_EDITING:
- installed_plugins = plugin_pool.get_all_plugins(placeholder, page)
- name = settings.CMS_PLACEHOLDER_CONF.get("%s %s" % (template, placeholder.slot), {}).get("name", None)
- if not name:
- name = settings.CMS_PLACEHOLDER_CONF.get(placeholder.slot, {}).get("name", None)
- if not name:
- name = placeholder.slot
- name = title(name)
- c.append(render_to_string("cms/toolbar/add_plugins.html", {'installed_plugins':installed_plugins,
- 'language':l,
- 'placeholder_label':name,
- 'placeholder':placeholder,
- 'page':page,
- }))
- from cms.middleware.toolbar import toolbar_plugin_processor
- processors = (toolbar_plugin_processor,)
- else:
- processors = None
-
- c.extend(render_plugins(plugins, context, placeholder, processors))
-
- return "".join(c)
View
4 cms/utils/plugins.py
@@ -5,6 +5,10 @@
from django.template.loader_tags import ConstantIncludeNode, ExtendsNode, BlockNode
from django.template import NodeList, TextNode, VariableNode
import warnings
+from django.shortcuts import get_object_or_404
+
+def get_page_from_plugin_or_404(cms_plugin):
+ return get_object_or_404(Page, placeholders=cms_plugin.placeholder)
def _extend_blocks(extend_node, blocks):
"""
View
797 mptt/managers--with rebuild.py
@@ -0,0 +1,797 @@
+"""
+A custom manager for working with trees of objects.
+"""
+from django.db import connection, models, transaction
+from django.utils.translation import ugettext as _
+
+from mptt.exceptions import InvalidMove
+
+__all__ = ('TreeManager',)
+
+qn = connection.ops.quote_name
+
+COUNT_SUBQUERY = """(
+ SELECT COUNT(*)
+ FROM %(rel_table)s
+ WHERE %(mptt_fk)s = %(mptt_table)s.%(mptt_pk)s
+)"""
+
+CUMULATIVE_COUNT_SUBQUERY = """(
+ SELECT COUNT(*)
+ FROM %(rel_table)s
+ WHERE %(mptt_fk)s IN
+ (
+ SELECT m2.%(mptt_pk)s
+ FROM %(mptt_table)s m2
+ WHERE m2.%(tree_id)s = %(mptt_table)s.%(tree_id)s
+ AND m2.%(left)s BETWEEN %(mptt_table)s.%(left)s
+ AND %(mptt_table)s.%(right)s
+ )
+)"""
+
+class TreeManager(models.Manager):
+ """
+ A manager for working with trees of objects.
+ """
+ def __init__(self, parent_attr, left_attr, right_attr, tree_id_attr,
+ level_attr):
+ """
+ Tree attributes for the model being managed are held as
+ attributes of this manager for later use, since it will be using
+ them a **lot**.
+ """
+ super(TreeManager, self).__init__()
+ self.parent_attr = parent_attr
+ self.left_attr = left_attr
+ self.right_attr = right_attr
+ self.tree_id_attr = tree_id_attr
+ self.level_attr = level_attr
+
+ def add_related_count(self, queryset, rel_model, rel_field, count_attr,
+ cumulative=False):
+ """
+ Adds a related item count to a given ``QuerySet`` using its
+ ``extra`` method, for a ``Model`` class which has a relation to
+ this ``Manager``'s ``Model`` class.
+
+ Arguments:
+
+ ``rel_model``
+ A ``Model`` class which has a relation to this `Manager``'s
+ ``Model`` class.
+
+ ``rel_field``
+ The name of the field in ``rel_model`` which holds the
+ relation.
+
+ ``count_attr``
+ The name of an attribute which should be added to each item in
+ this ``QuerySet``, containing a count of how many instances
+ of ``rel_model`` are related to it through ``rel_field``.
+
+ ``cumulative``
+ If ``True``, the count will be for each item and all of its
+ descendants, otherwise it will be for each item itself.
+ """
+ opts = self.model._meta
+ if cumulative:
+ subquery = CUMULATIVE_COUNT_SUBQUERY % {
+ 'rel_table': qn(rel_model._meta.db_table),
+ 'mptt_fk': qn(rel_model._meta.get_field(rel_field).column),
+ 'mptt_table': qn(opts.db_table),
+ 'mptt_pk': qn(opts.pk.column),
+ 'tree_id': qn(opts.get_field(self.tree_id_attr).column),
+ 'left': qn(opts.get_field(self.left_attr).column),
+ 'right': qn(opts.get_field(self.right_attr).column),
+ }
+ else:
+ subquery = COUNT_SUBQUERY % {
+ 'rel_table': qn(rel_model._meta.db_table),
+ 'mptt_fk': qn(rel_model._meta.get_field(rel_field).column),
+ 'mptt_table': qn(opts.db_table),
+ 'mptt_pk': qn(opts.pk.column),
+ }
+ return queryset.extra(select={count_attr: subquery})
+
+ def get_query_set(self):
+ """
+ Returns a ``QuerySet`` which contains all tree items, ordered in
+ such a way that that root nodes appear in tree id order and
+ their subtrees appear in depth-first order.
+ """
+ return super(TreeManager, self).get_query_set().order_by(
+ self.tree_id_attr, self.left_attr)
+
+ def insert_node(self, node, target, position='last-child',
+ commit=False):
+ """
+ Sets up the tree state for ``node`` (which has not yet been
+ inserted into in the database) so it will be positioned relative
+ to a given ``target`` node as specified by ``position`` (when
+ appropriate) it is inserted, with any neccessary space already
+ having been made for it.
+
+ A ``target`` of ``None`` indicates that ``node`` should be
+ the last root node.
+
+ If ``commit`` is ``True``, ``node``'s ``save()`` method will be
+ called before it is returned.
+ """
+ if node.pk:
+ raise ValueError(_('Cannot insert a node which has already been saved.'))
+
+ if target is None:
+ setattr(node, self.left_attr, 1)
+ setattr(node, self.right_attr, 2)
+ setattr(node, self.level_attr, 0)
+ setattr(node, self.tree_id_attr, self._get_next_tree_id())
+ setattr(node, self.parent_attr, None)
+ elif target.is_root_node() and position in ['left', 'right']:
+ target_tree_id = getattr(target, self.tree_id_attr)
+ if position == 'left':
+ tree_id = target_tree_id
+ space_target = target_tree_id - 1
+ else:
+ tree_id = target_tree_id + 1
+ space_target = target_tree_id
+
+ self._create_tree_space(space_target)
+
+ setattr(node, self.left_attr, 1)
+ setattr(node, self.right_attr, 2)
+ setattr(node, self.level_attr, 0)
+ setattr(node, self.tree_id_attr, tree_id)
+ setattr(node, self.parent_attr, None)
+ else:
+ setattr(node, self.left_attr, 0)
+ setattr(node, self.level_attr, 0)
+
+ space_target, level, left, parent = \
+ self._calculate_inter_tree_move_values(node, target, position)
+ tree_id = getattr(parent, self.tree_id_attr)
+
+ self._create_space(2, space_target, tree_id)
+
+ setattr(node, self.left_attr, -left)
+ setattr(node, self.right_attr, -left + 1)
+ setattr(node, self.level_attr, -level)
+ setattr(node, self.tree_id_attr, tree_id)
+ setattr(node, self.parent_attr, parent)
+
+ if commit:
+ node.save()
+ return node
+
+ def move_node(self, node, target, position='last-child'):
+ """
+ Moves ``node`` relative to a given ``target`` node as specified
+ by ``position`` (when appropriate), by examining both nodes and
+ calling the appropriate method to perform the move.
+
+ A ``target`` of ``None`` indicates that ``node`` should be
+ turned into a root node.
+
+ Valid values for ``position`` are ``'first-child'``,
+ ``'last-child'``, ``'left'`` or ``'right'``.
+
+ ``node`` will be modified to reflect its new tree state in the
+ database.
+
+ This method explicitly checks for ``node`` being made a sibling
+ of a root node, as this is a special case due to our use of tree
+ ids to order root nodes.
+ """
+ if target is None:
+ if node.is_child_node():
+ self._make_child_root_node(node)
+ elif target.is_root_node() and position in ['left', 'right']:
+ self._make_sibling_of_root_node(node, target, position)
+ else:
+ if node.is_root_node():
+ self._move_root_node(node, target, position)
+ else:
+ self._move_child_node(node, target, position)
+ transaction.commit_unless_managed()
+
+ def root_node(self, tree_id):
+ """
+ Returns the root node of the tree with the given id.
+ """
+ return self.get(**{
+ self.tree_id_attr: tree_id,
+ '%s__isnull' % self.parent_attr: True,
+ })
+
+ def root_nodes(self):
+ """
+ Creates a ``QuerySet`` containing root nodes.
+ """
+ return self.filter(**{'%s__isnull' % self.parent_attr: True})
+
+ def rebuild(self):
+ """
+ Rebuilds whole tree in database using `parent` link.
+ """
+ opts = self.model._meta
+
+ cursor = connection.cursor()
+ cursor.execute('UPDATE %(table)s SET %(left)s = 0, %(right)s = 0, %(level)s = 0, %(tree_id)s = 0' % {
+ 'table': qn(opts.db_table),
+ 'left': qn(opts.get_field(self.left_attr).column),
+ 'right': qn(opts.get_field(self.right_attr).column),
+ 'level': qn(opts.get_field(self.level_attr).column),
+ 'tree_id': qn(opts.get_field(self.tree_id_attr).column)
+ })
+
+ cursor.execute('SELECT %(id_col)s FROM %(table)s WHERE %(parent_col)s is NULL %(orderby)s' % {
+ 'id_col': qn(opts.pk.column),
+ 'table': qn(opts.db_table),
+ 'parent_col': qn(opts.get_field(self.parent_attr).column),
+ 'orderby': 'ORDER BY ' + ', '.join([qn(field) for field in opts.order_insertion_by]) if opts.order_insertion_by else ''
+ })
+
+ idx = 0
+ for (pk, ) in cursor.fetchall():
+ idx += 1
+ self._rebuild_helper(pk, 1, idx)
+ transaction.commit_unless_managed()
+
+ def _rebuild_helper(self, pk, left, tree_id, level=0):
+ opts = self.model._meta
+ right = left + 1
+
+ cursor = connection.cursor()
+ cursor.execute('SELECT %(id_col)s FROM %(table)s WHERE %(parent_col)s = %(parent)d %(orderby)s' % {
+ 'id_col': qn(opts.pk.column),
+ 'table': qn(opts.db_table),
+ 'parent_col': qn(opts.get_field(self.parent_attr).column),
+ 'parent': pk,
+ 'orderby': 'ORDER BY ' + ', '.join([qn(field) for field in opts.order_insertion_by]) if opts.order_insertion_by else ''
+ })
+
+ for (child_id, ) in cursor.fetchall():
+ right = self._rebuild_helper(child_id, right, tree_id, level+1)
+
+ cursor.execute("""
+ UPDATE %(table)s
+ SET
+ %(left_col)s = %(left)d,
+ %(right_col)s = %(right)d,
+ %(level_col)s = %(level)d,
+ %(tree_id_col)s = %(tree_id)d
+ WHERE
+ %(pk_col)s = %(pk)s
+ """ % {
+ 'table': qn(opts.db_table),
+ 'pk_col': qn(opts.pk.column),
+ 'left_col': qn(opts.get_field(self.left_attr).column),
+ 'right_col': qn(opts.get_field(self.right_attr).column),
+ 'level_col': qn(opts.get_field(self.level_attr).column),
+ 'tree_id_col': qn(opts.get_field(self.tree_id_attr).column),
+ 'pk': pk,
+ 'left': left,
+ 'right': right,
+ 'level': level,
+ 'tree_id': tree_id
+ })
+
+ return right + 1
+
+
+ def _calculate_inter_tree_move_values(self, node, target, position):
+ """
+ Calculates values required when moving ``node`` relative to
+ ``target`` as specified by ``position``.
+ """
+ left = getattr(node, self.left_attr)
+ level = getattr(node, self.level_attr)
+ target_left = getattr(target, self.left_attr)
+ target_right = getattr(target, self.right_attr)
+ target_level = getattr(target, self.level_attr)
+
+ if position == 'last-child' or position == 'first-child':
+ if position == 'last-child':
+ space_target = target_right - 1
+ else:
+ space_target = target_left
+ level_change = level - target_level - 1
+ parent = target
+ elif position == 'left' or position == 'right':
+ if position == 'left':
+ space_target = target_left - 1
+ else:
+ space_target = target_right
+ level_change = level - target_level
+ parent = getattr(target, self.parent_attr)
+ else:
+ raise ValueError(_('An invalid position was given: %s.') % position)
+
+ left_right_change = left - space_target - 1
+ return space_target, level_change, left_right_change, parent
+
+ def _close_gap(self, size, target, tree_id):
+ """
+ Closes a gap of a certain ``size`` after the given ``target``
+ point in the tree identified by ``tree_id``.
+ """
+ self._manage_space(-size, target, tree_id)
+
+ def _create_space(self, size, target, tree_id):
+ """
+ Creates a space of a certain ``size`` after the given ``target``
+ point in the tree identified by ``tree_id``.
+ """
+ self._manage_space(size, target, tree_id)
+
+ def _create_tree_space(self, target_tree_id):
+ """
+ Creates space for a new tree by incrementing all tree ids
+ greater than ``target_tree_id``.
+ """
+ opts = self.model._meta
+ cursor = connection.cursor()
+ cursor.execute("""
+ UPDATE %(table)s
+ SET %(tree_id)s = %(tree_id)s + 1
+ WHERE %(tree_id)s > %%s""" % {
+ 'table': qn(opts.db_table),
+ 'tree_id': qn(opts.get_field(self.tree_id_attr).column),
+ }, [target_tree_id])
+
+ def _get_next_tree_id(self):
+ """
+ Determines the next largest unused tree id for the tree managed
+ by this manager.
+ """
+ opts = self.model._meta
+ cursor = connection.cursor()
+ cursor.execute('SELECT MAX(%s) FROM %s' % (
+ qn(opts.get_field(self.tree_id_attr).column),
+ qn(opts.db_table)))
+ row = cursor.fetchone()
+ return row[0] and (row[0] + 1) or 1
+
+ def _inter_tree_move_and_close_gap(self, node, level_change,
+ left_right_change, new_tree_id, parent_pk=None):
+ """
+ Removes ``node`` from its current tree, with the given set of
+ changes being applied to ``node`` and its descendants, closing
+ the gap left by moving ``node`` as it does so.
+
+ If ``parent_pk`` is ``None``, this indicates that ``node`` is
+ being moved to a brand new tree as its root node, and will thus
+ have its parent field set to ``NULL``. Otherwise, ``node`` will
+ have ``parent_pk`` set for its parent field.
+ """
+ opts = self.model._meta
+ inter_tree_move_query = """
+ UPDATE %(table)s
+ SET %(level)s = CASE
+ WHEN %(left)s >= %%s AND %(left)s <= %%s
+ THEN %(level)s - %%s
+ ELSE %(level)s END,
+ %(tree_id)s = CASE
+ WHEN %(left)s >= %%s AND %(left)s <= %%s
+ THEN %%s
+ ELSE %(tree_id)s END,
+ %(left)s = CASE
+ WHEN %(left)s >= %%s AND %(left)s <= %%s
+ THEN %(left)s - %%s
+ WHEN %(left)s > %%s
+ THEN %(left)s - %%s
+ ELSE %(left)s END,
+ %(right)s = CASE
+ WHEN %(right)s >= %%s AND %(right)s <= %%s
+ THEN %(right)s - %%s
+ WHEN %(right)s > %%s
+ THEN %(right)s - %%s
+ ELSE %(right)s END,
+ %(parent)s = CASE
+ WHEN %(pk)s = %%s
+ THEN %(new_parent)s
+ ELSE %(parent)s END
+ WHERE %(tree_id)s = %%s""" % {
+ 'table': qn(opts.db_table),
+ 'level': qn(opts.get_field(self.level_attr).column),
+ 'left': qn(opts.get_field(self.left_attr).column),
+ 'tree_id': qn(opts.get_field(self.tree_id_attr).column),
+ 'right': qn(opts.get_field(self.right_attr).column),
+ 'parent': qn(opts.get_field(self.parent_attr).column),
+ 'pk': qn(opts.pk.column),
+ 'new_parent': parent_pk is None and 'NULL' or '%s',
+ }
+
+ left = getattr(node, self.left_attr)
+ right = getattr(node, self.right_attr)
+ gap_size = right - left + 1
+ gap_target_left = left - 1
+ params = [
+ left, right, level_change,
+ left, right, new_tree_id,
+ left, right, left_right_change,
+ gap_target_left, gap_size,
+ left, right, left_right_change,
+ gap_target_left, gap_size,
+ node.pk,
+ getattr(node, self.tree_id_attr)
+ ]
+ if parent_pk is not None:
+ params.insert(-1, parent_pk)
+ cursor = connection.cursor()
+ cursor.execute(inter_tree_move_query, params)
+
+ def _make_child_root_node(self, node, new_tree_id=None):
+ """
+ Removes ``node`` from its tree, making it the root node of a new
+ tree.
+
+ If ``new_tree_id`` is not specified a new tree id will be
+ generated.
+
+ ``node`` will be modified to reflect its new tree state in the
+ database.
+ """
+ left = getattr(node, self.left_attr)
+ right = getattr(node, self.right_attr)
+ level = getattr(node, self.level_attr)
+ tree_id = getattr(node, self.tree_id_attr)
+ if not new_tree_id:
+ new_tree_id = self._get_next_tree_id()
+ left_right_change = left - 1
+
+ self._inter_tree_move_and_close_gap(node, level, left_right_change,
+ new_tree_id)
+
+ # Update the node to be consistent with the updated
+ # tree in the database.
+ setattr(node, self.left_attr, left - left_right_change)
+ setattr(node, self.right_attr, right - left_right_change)
+ setattr(node, self.level_attr, 0)
+ setattr(node, self.tree_id_attr, new_tree_id)
+ setattr(node, self.parent_attr, None)
+
+ def _make_sibling_of_root_node(self, node, target, position):
+ """
+ Moves ``node``, making it a sibling of the given ``target`` root
+ node as specified by ``position``.
+
+ ``node`` will be modified to reflect its new tree state in the
+ database.
+
+ Since we use tree ids to reduce the number of rows affected by
+ tree mangement during insertion and deletion, root nodes are not
+ true siblings; thus, making an item a sibling of a root node is
+ a special case which involves shuffling tree ids around.
+ """
+ if node == target:
+ raise InvalidMove(_('A node may not be made a sibling of itself.'))
+
+ opts = self.model._meta
+ tree_id = getattr(node, self.tree_id_attr)
+ target_tree_id = getattr(target, self.tree_id_attr)
+
+ if node.is_child_node():
+ if position == 'left':
+ space_target = target_tree_id - 1
+ new_tree_id = target_tree_id
+ elif position == 'right':
+ space_target = target_tree_id
+ new_tree_id = target_tree_id + 1
+ else:
+ raise ValueError(_('An invalid position was given: %s.') % position)
+
+ self._create_tree_space(space_target)
+ if tree_id > space_target:
+ # The node's tree id has been incremented in the
+ # database - this change must be reflected in the node
+ # object for the method call below to operate on the
+ # correct tree.
+ setattr(node, self.tree_id_attr, tree_id + 1)
+ self._make_child_root_node(node, new_tree_id)
+ else:
+ if position == 'left':
+ if target_tree_id > tree_id:
+ left_sibling = target.get_previous_sibling()
+ if node == left_sibling:
+ return
+ new_tree_id = getattr(left_sibling, self.tree_id_attr)
+ lower_bound, upper_bound = tree_id, new_tree_id
+ shift = -1
+ else:
+ new_tree_id = target_tree_id
+ lower_bound, upper_bound = new_tree_id, tree_id
+ shift = 1
+ elif position == 'right':
+ if target_tree_id > tree_id:
+ new_tree_id = target_tree_id
+ lower_bound, upper_bound = tree_id, target_tree_id
+ shift = -1
+ else:
+ right_sibling = target.get_next_sibling()
+ if node == right_sibling:
+ return
+ new_tree_id = getattr(right_sibling, self.tree_id_attr)
+ lower_bound, upper_bound = new_tree_id, tree_id
+ shift = 1
+ else:
+ raise ValueError(_('An invalid position was given: %s.') % position)
+
+ root_sibling_query = """
+ UPDATE %(table)s
+ SET %(tree_id)s = CASE
+ WHEN %(tree_id)s = %%s
+ THEN %%s
+ ELSE %(tree_id)s + %%s END
+ WHERE %(tree_id)s >= %%s AND %(tree_id)s <= %%s""" % {
+ 'table': qn(opts.db_table),
+ 'tree_id': qn(opts.get_field(self.tree_id_attr).column),
+ }
+ cursor = connection.cursor()
+ cursor.execute(root_sibling_query, [tree_id, new_tree_id, shift,
+ lower_bound, upper_bound])
+ setattr(node, self.tree_id_attr, new_tree_id)
+
+ def _manage_space(self, size, target, tree_id):
+ """
+ Manages spaces in the tree identified by ``tree_id`` by changing
+ the values of the left and right columns by ``size`` after the
+ given ``target`` point.
+ """
+ opts = self.model._meta
+ space_query = """
+ UPDATE %(table)s
+ SET %(left)s = CASE
+ WHEN %(left)s > %%s
+ THEN %(left)s + %%s
+ ELSE %(left)s END,
+ %(right)s = CASE
+ WHEN %(right)s > %%s
+ THEN %(right)s + %%s
+ ELSE %(right)s END
+ WHERE %(tree_id)s = %%s
+ AND (%(left)s > %%s OR %(right)s > %%s)""" % {
+ 'table': qn(opts.db_table),
+ 'left': qn(opts.get_field(self.left_attr).column),
+ 'right': qn(opts.get_field(self.right_attr).column),
+ 'tree_id': qn(opts.get_field(self.tree_id_attr).column),
+ }
+ cursor = connection.cursor()
+ cursor.execute(space_query, [target, size, target, size, tree_id,
+ target, target])
+
+ def _move_child_node(self, node, target, position):
+ """
+ Calls the appropriate method to move child node ``node``
+ relative to the given ``target`` node as specified by
+ ``position``.
+ """
+ tree_id = getattr(node, self.tree_id_attr)
+ target_tree_id = getattr(target, self.tree_id_attr)
+
+ if (getattr(node, self.tree_id_attr) ==
+ getattr(target, self.tree_id_attr)):
+ self._move_child_within_tree(node, target, position)
+ else:
+ self._move_child_to_new_tree(node, target, position)
+
+ def _move_child_to_new_tree(self, node, target, position):
+ """
+ Moves child node ``node`` to a different tree, inserting it
+ relative to the given ``target`` node in the new tree as
+ specified by ``position``.
+
+ ``node`` will be modified to reflect its new tree state in the
+ database.
+ """
+ left = getattr(node, self.left_attr)
+ right = getattr(node, self.right_attr)
+ level = getattr(node, self.level_attr)
+ target_left = getattr(target, self.left_attr)
+ target_right = getattr(target, self.right_attr)
+ target_level = getattr(target, self.level_attr)
+ tree_id = getattr(node, self.tree_id_attr)
+ new_tree_id = getattr(target, self.tree_id_attr)
+
+ space_target, level_change, left_right_change, parent = \
+ self._calculate_inter_tree_move_values(node, target, position)
+
+ tree_width = right - left + 1
+
+ # Make space for the subtree which will be moved
+ self._create_space(tree_width, space_target, new_tree_id)
+ # Move the subtree
+ self._inter_tree_move_and_close_gap(node, level_change,
+ left_right_change, new_tree_id, parent.pk)
+
+ # Update the node to be consistent with the updated
+ # tree in the database.
+ setattr(node, self.left_attr, left - left_right_change)
+ setattr(node, self.right_attr, right - left_right_change)
+ setattr(node, self.level_attr, level - level_change)
+ setattr(node, self.tree_id_attr, new_tree_id)
+ setattr(node, self.parent_attr, parent)
+
+ def _move_child_within_tree(self, node, target, position):
+ """
+ Moves child node ``node`` within its current tree relative to
+ the given ``target`` node as specified by ``position``.
+
+ ``node`` will be modified to reflect its new tree state in the
+ database.
+ """
+ left = getattr(node, self.left_attr)
+ right = getattr(node, self.right_attr)
+ level = getattr(node, self.level_attr)
+ width = right - left + 1
+ tree_id = getattr(node, self.tree_id_attr)
+ target_left = getattr(target, self.left_attr)
+ target_right = getattr(target, self.right_attr)
+ target_level = getattr(target, self.level_attr)
+
+ if position == 'last-child' or position == 'first-child':
+ if node == target:
+ raise InvalidMove(_('A node may not be made a child of itself.'))
+ elif left < target_left < right:
+ raise InvalidMove(_('A node may not be made a child of any of its descendants.'))
+ if position == 'last-child':
+ if target_right > right:
+ new_left = target_right - width
+ new_right = target_right - 1
+ else:
+ new_left = target_right
+ new_right = target_right + width - 1
+ else:
+ if target_left > left:
+ new_left = target_left - width + 1
+ new_right = target_left
+ else:
+ new_left = target_left + 1
+ new_right = target_left + width
+ level_change = level - target_level - 1
+ parent = target
+ elif position == 'left' or position == 'right':
+ if node == target:
+ raise InvalidMove(_('A node may not be made a sibling of itself.'))
+ elif left < target_left < right:
+ raise InvalidMove(_('A node may not be made a sibling of any of its descendants.'))
+ if position == 'left':
+ if target_left > left:
+ new_left = target_left - width
+ new_right = target_left - 1
+ else:
+ new_left = target_left
+ new_right = target_left + width - 1
+ else:
+ if target_right > right:
+ new_left = target_right - width + 1
+ new_right = target_right
+ else:
+ new_left = target_right + 1
+ new_right = target_right + width
+ level_change = level - target_level
+ parent = getattr(target, self.parent_attr)
+ else:
+ raise ValueError(_('An invalid position was given: %s.') % position)
+
+ left_boundary = min(left, new_left)
+ right_boundary = max(right, new_right)
+ left_right_change = new_left - left
+ gap_size = width
+ if left_right_change > 0:
+ gap_size = -gap_size
+
+ opts = self.model._meta
+ # The level update must come before the left update to keep
+ # MySQL happy - left seems to refer to the updated value
+ # immediately after its update has been specified in the query
+ # with MySQL, but not with SQLite or Postgres.
+ move_subtree_query = """
+ UPDATE %(table)s
+ SET %(level)s = CASE
+ WHEN %(left)s >= %%s AND %(left)s <= %%s
+ THEN %(level)s - %%s
+ ELSE %(level)s END,
+ %(left)s = CASE
+ WHEN %(left)s >= %%s AND %(left)s <= %%s
+ THEN %(left)s + %%s
+ WHEN %(left)s >= %%s AND %(left)s <= %%s
+ THEN %(left)s + %%s
+ ELSE %(left)s END,
+ %(right)s = CASE
+ WHEN %(right)s >= %%s AND %(right)s <= %%s
+ THEN %(right)s + %%s
+ WHEN %(right)s >= %%s AND %(right)s <= %%s
+ THEN %(right)s + %%s
+ ELSE %(right)s END,
+ %(parent)s = CASE
+ WHEN %(pk)s = %%s
+ THEN %%s
+ ELSE %(parent)s END
+ WHERE %(tree_id)s = %%s""" % {
+ 'table': qn(opts.db_table),
+ 'level': qn(opts.get_field(self.level_attr).column),
+ 'left': qn(opts.get_field(self.left_attr).column),
+ 'right': qn(opts.get_field(self.right_attr).column),
+ 'parent': qn(opts.get_field(self.parent_attr).column),
+ 'pk': qn(opts.pk.column),
+ 'tree_id': qn(opts.get_field(self.tree_id_attr).column),
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(move_subtree_query, [
+ left, right, level_change,
+ left, right, left_right_change,
+ left_boundary, right_boundary, gap_size,
+ left, right, left_right_change,
+ left_boundary, right_boundary, gap_size,
+ node.pk, parent.pk,
+ tree_id])
+
+ # Update the node to be consistent with the updated
+ # tree in the database.
+ setattr(node, self.left_attr, new_left)
+ setattr(node, self.right_attr, new_right)
+ setattr(node, self.level_attr, level - level_change)
+ setattr(node, self.parent_attr, parent)
+
+ def _move_root_node(self, node, target, position):
+ """
+ Moves root node``node`` to a different tree, inserting it
+ relative to the given ``target`` node as specified by
+ ``position``.
+
+ ``node`` will be modified to reflect its new tree state in the
+ database.
+ """
+ left = getattr(node, self.left_attr)
+ right = getattr(node, self.right_attr)
+ level = getattr(node, self.level_attr)
+ tree_id = getattr(node, self.tree_id_attr)
+ new_tree_id = getattr(target, self.tree_id_attr)
+ width = right - left + 1
+
+ if node == target:
+ raise InvalidMove(_('A node may not be made a child of itself.'))
+ elif tree_id == new_tree_id:
+ raise InvalidMove(_('A node may not be made a child of any of its descendants.'))
+
+ space_target, level_change, left_right_change, parent = \
+ self._calculate_inter_tree_move_values(node, target, position)
+
+ # Create space for the tree which will be inserted
+ self._create_space(width, space_target, new_tree_id)
+
+ # Move the root node, making it a child node
+ opts = self.model._meta
+ move_tree_query = """
+ UPDATE %(table)s
+ SET %(level)s = %(level)s - %%s,
+ %(left)s = %(left)s - %%s,
+ %(right)s = %(right)s - %%s,
+ %(tree_id)s = %%s,
+ %(parent)s = CASE
+ WHEN %(pk)s = %%s
+ THEN %%s
+ ELSE %(parent)s END
+ WHERE %(left)s >= %%s AND %(left)s <= %%s
+ AND %(tree_id)s = %%s""" % {
+ 'table': qn(opts.db_table),
+ 'level': qn(opts.get_field(self.level_attr).column),
+ 'left': qn(opts.get_field(self.left_attr).column),
+ 'right': qn(opts.get_field(self.right_attr).column),
+ 'tree_id': qn(opts.get_field(self.tree_id_attr).column),
+ 'parent': qn(opts.get_field(self.parent_attr).column),
+ 'pk': qn(opts.pk.column),
+ }
+ cursor = connection.cursor()
+ cursor.execute(move_tree_query, [level_change, left_right_change,
+ left_right_change, new_tree_id, node.pk, parent.pk, left, right,
+ tree_id])
+
+ # Update the former root node to be consistent with the updated
+ # tree in the database.
+ setattr(node, self.left_attr, left - left_right_change)
+ setattr(node, self.right_attr, right - left_right_change)
+ setattr(node, self.level_attr, level - level_change)
+ setattr(node, self.tree_id_attr, new_tree_id)
+ setattr(node, self.parent_attr, parent)
Please sign in to comment.
Something went wrong with that request. Please try again.