Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'hotfix/2.1.3'

  • Loading branch information...
commit 6db7026376177434472b7583ba88f183b5e96790 2 parents 614297c + 9d61907
@ojii ojii authored
View
3  AUTHORS
@@ -57,6 +57,7 @@ Contributors (in alphabetical order):
* Gerard Świderski
* homebrew79
* hysia
+* Iacopo Spalletti
* Ian Lewis
* indexofire
* Ionel Maries Cristian
@@ -157,4 +158,4 @@ Contributors (in alphabetical order):
* Yosuke Ikeda
* Yuri van der Meer
* zundoya
-* Антон Евжаков
+* Антон Евжаков
View
9 CHANGELOG.txt
@@ -43,4 +43,11 @@
==== 2.1.2 (2011-02-16) ====
- Fixed issues with the CSRF fix from 2.1.1.
-- Updated translation files from transifex.
+- Updated translation files from transifex.
+
+==== 2.1.3 (2011-02-22) ====
+
+- Fixed a serious security issue in PlaceholderAdmin
+- Fixed bug with submenus showing pages that are not 'in_navigation' (#716, thanks to Iacopo Spalletti for the patch)
+- Fixed PlaceholderField not respecting limits in CMS_PLACEHOLDER_CONF (thanks to Ben Hockey for reporting this)
+- Fixed the double-monkeypatch check for url reversing (thanks to Benjamin Wohlwend for the patch)
View
2  cms/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-VERSION = (2, 1, 2, 'final')
+VERSION = (2, 1, 3, 'final')
if VERSION[-1] != "final": # pragma: no cover
__version__ = '.'.join(map(str, VERSION))
else: # pragma: no cover
View
19 cms/admin/pageadmin.py
@@ -1074,6 +1074,9 @@ def change_innavigation(self, request, page_id):
@create_on_success
def add_plugin(self, request):
+ '''
+ Could be either a page or a parent - if it's a parent we get the page via parent.
+ '''
if 'history' in request.path or 'recover' in request.path:
return HttpResponse(str("error"))
if request.method == "POST":
@@ -1108,14 +1111,20 @@ def add_plugin(self, request):
parent = get_object_or_404(CMSPlugin, pk=parent_id)
placeholder = parent.placeholder
page = get_page_from_placeholder_if_exists(placeholder)
+ if not page: # Make sure we do have a page
+ raise Http404
language = parent.language
position = None
# placeholder (non-page) add-plugin
else:
- position = None
- language = request.POST['language'] or get_language_from_request(request)
- if page and not page.has_change_permission(request):
- return HttpResponseForbidden(unicode(_("You do not have permission to change this page")))
+ # do NOT allow non-page placeholders to use this method, they
+ # should use their respective admin!
+ raise Http404
+
+ if not page.has_change_permission(request):
+ # we raise a 404 instead of 403 for a slightly improved security
+ # and to be consistent with placeholder admin
+ raise Http404
# Sanity check to make sure we're not getting bogus values from JavaScript:
if not language or not language in [ l[0] for l in settings.LANGUAGES ]:
@@ -1175,7 +1184,7 @@ def edit_plugin(self, request, plugin_id):
page = get_page_from_placeholder_if_exists(cms_plugin.placeholder)
instance, plugin_admin = cms_plugin.get_plugin_instance(self.admin_site)
if page and not page.has_change_permission(request):
- raise PermissionDenied
+ raise Http404
else:
# history view with reversion
from reversion.models import Version
View
137 cms/admin/placeholderadmin.py
@@ -122,29 +122,66 @@ def get_urls(self):
return url_patterns + super(PlaceholderAdmin, self).get_urls()
def add_plugin(self, request):
+ # only allow POST
if request.method != "POST":
raise Http404
plugin_type = request.POST['plugin_type']
placeholder_id = request.POST.get('placeholder', None)
position = None
language = get_language_from_request(request)
- if not placeholder_id:
+ parent = None
+ # check if we got a placeholder (id)
+ if placeholder_id:
+ placeholder = get_object_or_404(Placeholder, pk=placeholder_id)
+ else: # else get the parent_id
parent_id = request.POST.get('parent_id', None)
- if not parent_id:
+ if not parent_id: # if we get neither a placeholder nor a parent, bail out
raise Http404
parent = get_object_or_404(CMSPlugin, pk=parent_id)
- plugin = CMSPlugin(language=language, plugin_type=plugin_type,
- position=position, parent=parent, placeholder=parent.placeholder)
- else:
- placeholder = get_object_or_404(Placeholder, pk=placeholder_id)
- plugin = CMSPlugin(language=language, plugin_type=plugin_type,
- position=position, placeholder=placeholder)
+ placeholder = parent.placeholder
+
+ # check add permissions on placeholder
+ if not placeholder.has_add_permission(request):
+ raise Http404
+
+ # check the limits defined in CMS_PLACEHOLDER_CONF for this placeholder
+ limits = settings.CMS_PLACEHOLDER_CONF.get(placeholder.slot, {}).get('limits', None)
+ if limits:
+ count = placeholder.cmsplugin_set.count()
+ global_limit = limits.get("global", None)
+ type_limit = limits.get(plugin_type, None)
+ # check the global limit first
+ if global_limit and count >= global_limit:
+ return HttpResponseBadRequest(
+ "This placeholder already has the maximum number of plugins."
+ )
+ elif type_limit: # then check the type specific limit
+ type_count = CMSPlugin.objects.filter(
+ language=language, placeholder=placeholder, plugin_type=plugin_type
+ ).count()
+ if type_count >= type_limit:
+ return HttpResponseBadRequest(
+ "This placeholder already has the maximum number (%s) "
+ "of %s plugins." % (type_limit, plugin_type)
+ )
+
+ # actually add the plugin
+ plugin = CMSPlugin(language=language, plugin_type=plugin_type,
+ position=position, placeholder=placeholder, parent=parent)
plugin.save()
+
+ # returns it's ID as response
return HttpResponse(str(plugin.pk))
def edit_plugin(self, request, plugin_id):
plugin_id = int(plugin_id)
+ # get the plugin to edit of bail out
cms_plugin = get_object_or_404(CMSPlugin, pk=plugin_id)
+
+ # check that the user has permission to change this plugin
+ if not cms_plugin.placeholder.has_change_permission(request):
+ raise Http404
+
instance, plugin_admin = cms_plugin.get_plugin_instance(self.admin_site)
plugin_admin.cms_plugin_instance = cms_plugin
@@ -185,46 +222,72 @@ def edit_plugin(self, request, plugin_id):
return response
def move_plugin(self, request):
- if request.method == "POST":
- pos = 0
- if 'ids' in request.POST:
- for id in request.POST['ids'].split("_"):
- plugin = CMSPlugin.objects.get(pk=id)
- if plugin.position != pos:
- plugin.position = pos
- plugin.save()
- pos += 1
- elif 'plugin_id' in request.POST:
- plugin = CMSPlugin.objects.get(pk=int(request.POST['plugin_id']))
- placeholder = plugin.placeholder
- # plugin positions are 0 based, so just using count here should give us 'last_position + 1'
- position = CMSPlugin.objects.filter(placeholder=placeholder).count()
- plugin.position = position
- plugin.save()
- else:
- HttpResponse(str("error"))
- return HttpResponse(str("ok"))
- else:
+ # only allow POST
+ if request.method != "POST":
return HttpResponse(str("error"))
+ pos = 0
+ if 'ids' in request.POST: # multiple plugins
+ whitelisted_placeholders = []
+ for id in request.POST['ids'].split("_"):
+ plugin = CMSPlugin.objects.get(pk=id)
+
+ # check the permissions for *each* plugin, but cache them locally
+ # per placeholder
+ if plugin.placeholder.pk not in whitelisted_placeholders:
+ if plugin.placeholder.has_change_permission(request):
+ whitelisted_placeholders.append(plugin.placeholder.pk)
+ else:
+ raise Http404
+
+ # actually do the moving
+ if plugin.position != pos:
+ plugin.position = pos
+ plugin.save()
+ pos += 1
+ elif 'plugin_id' in request.POST: # single plugin moving
+ plugin = CMSPlugin.objects.get(pk=int(request.POST['plugin_id']))
- def remove_plugin(self, request):
- if request.method == "POST":
- plugin_id = request.POST['plugin_id']
- plugin = get_object_or_404(CMSPlugin, pk=plugin_id)
+ # check permissions
+ if not plugin.placeholder.has_change_permission(request):
+ raise Http404
+
placeholder = plugin.placeholder
- plugin.delete_with_public()
- plugin_name = unicode(plugin_pool.get_plugin(plugin.plugin_type).name)
- comment = _(u"%(plugin_name)s plugin at position %(position)s in %(placeholder)s was deleted.") % {'plugin_name':plugin_name, 'position':plugin.position, 'placeholder':plugin.placeholder}
- return HttpResponse("%s,%s" % (plugin_id, comment))
- raise Http404
+ # plugin positions are 0 based, so just using count here should give us 'last_position + 1'
+ position = CMSPlugin.objects.filter(placeholder=placeholder).count()
+ plugin.position = position
+ plugin.save()
+ else:
+ HttpResponse(str("error"))
+ return HttpResponse(str("ok"))
+
+ def remove_plugin(self, request):
+ if request.method != "POST": # only allow POST
+ raise Http404
+ plugin_id = request.POST['plugin_id']
+ plugin = get_object_or_404(CMSPlugin, pk=plugin_id)
+
+ # check the permissions!
+ if not plugin.placeholder.has_delete_permission(request):
+ raise Http404
+
+ plugin.delete_with_public()
+ plugin_name = unicode(plugin_pool.get_plugin(plugin.plugin_type).name)
+ comment = _(u"%(plugin_name)s plugin at position %(position)s in %(placeholder)s was deleted.") % {'plugin_name':plugin_name, 'position':plugin.position, 'placeholder':plugin.placeholder}
+ return HttpResponse("%s,%s" % (plugin_id, comment))
def copy_plugins(self, request):
+ # only allow POST
if request.method != "POST":
raise Http404
placeholder_id = request.POST['placeholder']
placeholder = get_object_or_404(Placeholder, pk=placeholder_id)
+ # check permissions
+ if not placeholder.has_add_permission(request):
+ raise Http404
+ # the placeholder actions are responsible for copying, they should return
+ # a list of plugins if successful.
plugins = placeholder.actions.copy(
target_placeholder=placeholder,
source_language=request.POST['copy_from'],
View
11 cms/models/__init__.py
@@ -37,6 +37,8 @@ def remove_current_root(url):
return url
def monkeypatch_reverse():
+ if hasattr(django.core.urlresolvers.reverse, 'cms_monkeypatched'):
+ return
django.core.urlresolvers.old_reverse = django.core.urlresolvers.reverse
def new_reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None):
@@ -62,13 +64,10 @@ def new_reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, cur
raise e
url = remove_current_root(url)
return url
-
+
+ new_reverse.cms_monkeypatched = True
django.core.urlresolvers.reverse = new_reverse
validate_dependencies()
validate_settings()
-
-monkeypatched = False
-if not monkeypatched:
- monkeypatch_reverse()
- monkeypatched = True
+monkeypatch_reverse()
View
54 cms/models/placeholdermodel.py
@@ -11,17 +11,40 @@ 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'
- def has_change_permission(self, request):
- opts = self._meta
+ def __unicode__(self):
+ return self.slot
+
+ def _get_permission(self, request, key):
+ """
+ Generic method to check the permissions for a request for a given key,
+ the key can be: 'add', 'change' or 'delete'.
+ """
if request.user.is_superuser:
return True
- return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
+ found = False
+ # check all attached models for change permissions
+ for model in self._get_attached_models():
+ opts = model._meta
+ perm_accessor = getattr(opts, 'get_%s_permission' % key)
+ perm_code = '%s.%s' % (opts.app_label, perm_accessor())
+ # if they don't have the permission for this attached model, bail out
+ if not request.user.has_perm(perm_code):
+ return False
+ else:
+ found = True
+ return found
+
+ def has_change_permission(self, request):
+ return self._get_permission(request, 'change')
+
+ def has_add_permission(self, request):
+ return self._get_permission(request, 'add')
+
+ def has_delete_permission(self, request):
+ return self._get_permission(request, 'delete')
def render(self, context, width):
from cms.plugin_rendering import render_placeholder
@@ -37,6 +60,18 @@ def get_media(self, request, context):
return reduce(operator.add, media_classes)
return Media()
+ def _get_attached_fields(self):
+ """
+ Returns an ITERATOR of all non-cmsplugin reverse foreign key related fields.
+ """
+ from cms.models import CMSPlugin
+ for rel in self._meta.get_all_related_objects():
+ if isinstance(rel.model, CMSPlugin):
+ continue
+ field = getattr(self, rel.get_accessor_name())
+ if field.count():
+ yield rel.field
+
def _get_attached_field(self):
from cms.models import CMSPlugin
if not hasattr(self, '_attached_field_cache'):
@@ -60,6 +95,13 @@ def _get_attached_model(self):
if field:
return field.model
return None
+
+ def _get_attached_models(self):
+ """
+ Returns a list of models of attached to this placeholder.
+ """
+ return [field.model for field in self._get_attached_fields()]
+
def get_plugins_list(self):
return list(self.get_plugins())
View
985 cms/test/testcases.py
@@ -1,487 +1,498 @@
-# -*- coding: utf-8 -*-
-from cms.admin.forms import save_permissions
-from cms.models import Title, Page
-from cms.models.moderatormodels import ACCESS_PAGE_AND_DESCENDANTS
-from cms.models.permissionmodels import PagePermission, PageUser
-from cms.models.pluginmodel import CMSPlugin
-from cms.plugins.text.models import Text
-from cms.test.util.context_managers import UserLoginContext, SettingsOverride
-from cms.utils.permissions import _thread_locals
-from django.conf import settings
-from django.contrib.auth.models import User, AnonymousUser
-from django.contrib.sites.models import Site
-from django.core.exceptions import ObjectDoesNotExist
-from django.core.handlers.wsgi import WSGIRequest
-from django.core.urlresolvers import reverse
-from django.template.context import Context
-from django.template.defaultfilters import slugify
-from django.test.testcases import TestCase
-from menus.menu_pool import menu_pool
-from urlparse import urlparse
-import sys
-import urllib
-import warnings
-
-
-URL_CMS_PAGE = "/admin/cms/page/"
-URL_CMS_PAGE_ADD = URL_CMS_PAGE + "add/"
-URL_CMS_PAGE_CHANGE = URL_CMS_PAGE + "%d/"
-URL_CMS_PAGE_DELETE = URL_CMS_PAGE_CHANGE + "delete/"
-URL_CMS_PLUGIN_ADD = URL_CMS_PAGE + "add-plugin/"
-URL_CMS_PLUGIN_EDIT = URL_CMS_PAGE + "edit-plugin/"
-URL_CMS_PLUGIN_REMOVE = URL_CMS_PAGE + "remove-plugin/"
-
-class _Warning(object):
- def __init__(self, message, category, filename, lineno):
- self.message = message
- self.category = category
- self.filename = filename
- self.lineno = lineno
-
-
-
-def _collectWarnings(observeWarning, f, *args, **kwargs):
- def showWarning(message, category, filename, lineno, file=None, line=None):
- assert isinstance(message, Warning)
- observeWarning(_Warning(
- message.args[0], category, filename, lineno))
-
- # Disable the per-module cache for every module otherwise if the warning
- # which the caller is expecting us to collect was already emitted it won't
- # be re-emitted by the call to f which happens below.
- for v in sys.modules.itervalues():
- if v is not None:
- try:
- v.__warningregistry__ = None
- except:
- # Don't specify a particular exception type to handle in case
- # some wacky object raises some wacky exception in response to
- # the setattr attempt.
- pass
-
- origFilters = warnings.filters[:]
- origShow = warnings.showwarning
- warnings.simplefilter('always')
- try:
- warnings.showwarning = showWarning
- result = f(*args, **kwargs)
- finally:
- warnings.filters[:] = origFilters
- warnings.showwarning = origShow
- return result
-
-class CMSTestCase(TestCase):
- counter = 1
-
- def _post_teardown(self):
- # Needed to clean the menu keys cache, see menu.menu_pool.clear()
- menu_pool.clear()
- super(CMSTestCase, self)._post_teardown()
-
- def login_user(self, user):
- logged_in = self.client.login(username=user.username, password=user.username)
- self.user = user
- self.assertEqual(logged_in, True)
-
- def login_user_context(self, user):
- return UserLoginContext(self, user)
-
- def get_superuser(self):
- admin = User(username="admin", is_staff=True, is_active=True, is_superuser=True)
- admin.set_password("admin")
- admin.save()
- return admin
-
- def get_new_page_data(self, parent_id=''):
- page_data = {'title':'test page %d' % self.counter,
- 'slug':'test-page-%d' % self.counter, 'language':settings.LANGUAGES[0][0],
- 'site':1, 'template':'nav_playground.html', 'parent': parent_id}
-
- # required only if user haves can_change_permission
- page_data['pagepermission_set-TOTAL_FORMS'] = 0
- page_data['pagepermission_set-INITIAL_FORMS'] = 0
- page_data['pagepermission_set-MAX_NUM_FORMS'] = 0
-
- self.counter = self.counter + 1
- return page_data
-
- def print_page_structure(self, title=None):
- """Just a helper to see the page struct.
- """
- for page in Page.objects.drafts().order_by('tree_id', 'lft'):
- print "%s%s #%d" % (" " * (page.level), page, page.id)
-
- def print_node_structure(self, nodes, *extra):
- def _rec(nodes, level=0):
- ident = level * ' '
- for node in nodes:
- raw_attrs = [(bit, getattr(node, bit, node.attr.get(bit, "unknown"))) for bit in extra]
- attrs = ', '.join(['%s: %r' % data for data in raw_attrs])
- print "%s%s: %s" % (ident, node.title, attrs)
- _rec(node.children, level+1)
- _rec(nodes)
-
- def assertObjectExist(self, qs, **filter):
- try:
- return qs.get(**filter)
- except ObjectDoesNotExist:
- pass
- raise self.failureException, "ObjectDoesNotExist raised"
-
- def assertObjectDoesNotExist(self, qs, **filter):
- try:
- qs.get(**filter)
- except ObjectDoesNotExist:
- return
- raise self.failureException, "ObjectDoesNotExist not raised"
-
- def create_page(self, parent_page=None, user=None, position="last-child",
- title=None, site=1, published=False, in_navigation=False,
- moderate=False, language=None, title_extra=None, **extra):
- """
- Common way for page creation with some checks
- """
- _thread_locals.user = user
- if not language:
- language = settings.LANGUAGES[0][0]
- if settings.CMS_SITE_LANGUAGES.get(site, False):
- language = settings.CMS_SITE_LANGUAGES[site][0]
- site = Site.objects.get(pk=site)
-
- page_data = {
- 'site': site,
- 'template': 'nav_playground.html',
- 'published': published,
- 'in_navigation': in_navigation,
- }
- if user:
- page_data['created_by'] = user
- page_data['changed_by'] = user
- if parent_page:
- page_data['parent'] = parent_page
- page_data.update(extra)
-
- page = Page(**page_data)
- if parent_page:
- page.insert_at(self.reload(parent_page), position)
- page.save()
-
- if settings.CMS_MODERATOR and user:
- page.pagemoderator_set.create(user=user)
-
- if not title:
- title = 'test page %d' % self.counter
- slug = 'test-page-%d' % self.counter
- else:
- slug = slugify(title)
- self.counter = self.counter + 1
- if not title_extra:
- title_extra = {}
- self.create_title(
- title=title,
- slug=slug,
- language=language,
- page=page,
- **title_extra
- )
-
- del _thread_locals.user
- return page
-
- def create_title(self, title, slug, language, page, **extra):
- return Title.objects.create(
- title=title,
- slug=slug,
- language=language,
- page=page,
- **extra
- )
-
- def copy_page(self, page, target_page):
- from cms.utils.page import get_available_slug
-
- data = {
- 'position': 'last-child',
- 'target': target_page.pk,
- 'site': 1,
- 'copy_permissions': 'on',
- 'copy_moderation': 'on',
- }
-
- response = self.client.post(URL_CMS_PAGE + "%d/copy-page/" % page.pk, data)
- self.assertEquals(response.status_code, 200)
- self.assertEquals(response.content, "ok")
-
- title = page.title_set.all()[0]
- copied_slug = get_available_slug(title)
-
- copied_page = self.assertObjectExist(Page.objects, title_set__slug=copied_slug, parent=target_page)
- return copied_page
-
- def move_page(self, page, target_page, position="first-child"):
- page.move_page(target_page, position)
- return self.reload_page(page)
-
- def reload_page(self, page):
- """
- Returns a fresh instance of the page from the database
- """
- return self.reload(page)
-
- def reload(self, obj):
- return obj.__class__.objects.get(pk=obj.pk)
-
- def get_pages_root(self):
- return urllib.unquote(reverse("pages-root"))
-
- def get_context(self, path=None):
- if not path:
- path = self.get_pages_root()
- context = {}
- request = self.get_request(path)
-
- context['request'] = request
-
- return Context(context)
-
- def get_request(self, path=None, language=settings.LANGUAGES[0][0]):
- if not path:
- path = self.get_pages_root()
-
- parsed_path = urlparse(path)
- host = parsed_path.netloc or 'testserver'
- port = 80
- if ':' in host:
- host, port = host.split(':', 1)
-
- environ = {
- 'HTTP_COOKIE': self.client.cookies,
- 'PATH_INFO': parsed_path.path,
- 'QUERY_STRING': parsed_path.query,
- 'REMOTE_ADDR': '127.0.0.1',
- 'REQUEST_METHOD': 'GET',
- 'SCRIPT_NAME': '',
- 'SERVER_NAME': host,
- 'SERVER_PORT': port,
- 'SERVER_PROTOCOL': 'HTTP/1.1',
- 'wsgi.version': (1,0),
- 'wsgi.url_scheme': 'http',
- 'wsgi.errors': self.client.errors,
- 'wsgi.multiprocess': True,
- 'wsgi.multithread': False,
- 'wsgi.run_once': False,
- }
- request = WSGIRequest(environ)
- request.session = self.client.session
- request.user = getattr(self, 'user', AnonymousUser())
- request.LANGUAGE_CODE = language
- return request
-
- def create_page_user(self, username, password=None,
- can_add_page=True, can_change_page=True, can_delete_page=True,
- can_recover_page=True, can_add_pageuser=True, can_change_pageuser=True,
- can_delete_pageuser=True, can_add_pagepermission=True,
- can_change_pagepermission=True, can_delete_pagepermission=True,
- grant_all=False):
- """
- Helper function for creating page user, through form on:
- /admin/cms/pageuser/add/
-
- Returns created user.
- """
- if grant_all:
- return self.create_page_user(username, password,
- True, True, True, True, True, True, True, True, True, True)
-
- if password is None:
- password=username
-
- data = {
- 'can_add_page': can_add_page,
- 'can_change_page': can_change_page,
- 'can_delete_page': can_delete_page,
- 'can_recover_page': can_recover_page,
- 'can_add_pageuser': can_add_pageuser,
- 'can_change_pageuser': can_change_pageuser,
- 'can_delete_pageuser': can_delete_pageuser,
- 'can_add_pagepermission': can_add_pagepermission,
- 'can_change_pagepermission': can_change_pagepermission,
- 'can_delete_pagepermission': can_delete_pagepermission,
- }
- if hasattr(self, 'user'):
- created_by = self.user
- else:
- created_by = User.objects.create_superuser('superuser', 'superuser@django-cms.org', 'superuser')
- try:
- user = User.objects.get(username=username)
- except User.DoesNotExist:
- user = User.objects.create_user(username, 'username@django-cms.org', password)
- user.is_staff = True
- user.is_active = True
- page_user = PageUser(created_by=created_by)
- for field in [f.name for f in User._meta.local_fields]:
- setattr(page_user, field, getattr(user, field))
- user.save()
- page_user.save()
- save_permissions(data, page_user)
- return user
-
- def assign_user_to_page(self, page, user, grant_on=ACCESS_PAGE_AND_DESCENDANTS,
- can_add=False, can_change=False, can_delete=False,
- can_change_advanced_settings=False, can_publish=False,
- can_change_permissions=False, can_move_page=False, can_moderate=False,
- grant_all=False):
- """Assigns given user to page, and gives him requested permissions.
-
- Note: this is not happening over frontend, maybe a test for this in
- future will be nice.
- """
- if grant_all:
- return self.assign_user_to_page(page, user, grant_on,
- True, True, True, True, True, True, True, True)
-
- data = {
- 'can_add': can_add,
- 'can_change': can_change,
- 'can_delete': can_delete,
- 'can_change_advanced_settings': can_change_advanced_settings,
- 'can_publish': can_publish,
- 'can_change_permissions': can_change_permissions,
- 'can_move_page': can_move_page,
- 'can_moderate': can_moderate,
- }
-
- page_permission = PagePermission(page=page, user=user, grant_on=grant_on, **data)
- page_permission.save()
- return page_permission
-
- def add_plugin(self, user=None, page=None, placeholder=None, language='en', body=''):
- if not placeholder:
- if page:
- placeholder = page.placeholders.get(slot__iexact='Right-Column')
- else:
- placeholder = page.placeholders.get(slot__iexact='Right-Column')
-
- plugin_base = CMSPlugin(
- plugin_type='TextPlugin',
- placeholder=placeholder,
- position=1,
- language=language
- )
- plugin_base.insert_at(None, position='last-child', commit=False)
-
- plugin = Text(body=body)
- plugin_base.set_base_attr(plugin)
- plugin.save()
- return plugin.pk
-
- def publish_page(self, page, approve=False, user=None, published_check=True):
- if user:
- self.login_user(user)
-
- if published_check and not approve:
- # must have public object now
- self.assertFalse(page.publisher_public)
- self.assertFalse(page.published)
-
- # publish / approve page by master
- response = self.client.post(URL_CMS_PAGE + "%d/change-status/" % page.pk, {1 :1})
- self.assertEqual(response.status_code, 200)
-
- if not approve:
- page = self.reload_page(page)
- if published_check:
- # must have public object now
- self.assertTrue(page.publisher_public, "Page '%s' has no publisher_public" % page)
- # and public object must be published
- self.assertTrue(page.publisher_public.published)
- return page
-
- # approve
- page = self.approve_page(page)
- if published_check:
- # must have public object now
- self.assertTrue(page.publisher_public, "Page '%s' has no publisher_public" % page)
- # and public object must be published
- self.assertTrue(page.publisher_public.published)
-
- return page
-
- def approve_page(self, page):
- response = self.client.get(URL_CMS_PAGE + "%d/approve/" % page.pk)
- self.assertRedirects(response, URL_CMS_PAGE)
- # reload page
- return self.reload_page(page)
-
- def check_published_page_attributes(self, page):
- public_page = page.publisher_public
-
- if page.parent:
- self.assertEqual(page.parent_id, public_page.parent.publisher_draft.id)
-
- self.assertEqual(page.level, public_page.level)
-
- # TODO: add check for siblings
-
- draft_siblings = list(page.get_siblings(True). \
- filter(publisher_is_draft=True).order_by('tree_id', 'parent', 'lft'))
- public_siblings = list(public_page.get_siblings(True). \
- filter(publisher_is_draft=False).order_by('tree_id', 'parent', 'lft'))
-
- skip = 0
- for i, sibling in enumerate(draft_siblings):
- if not sibling.publisher_public_id:
- skip += 1
- continue
- self.assertEqual(sibling.id, public_siblings[i - skip].publisher_draft.id)
-
- def request_moderation(self, page, level):
- """Assign current logged in user to the moderators / change moderation
-
- Args:
- page: Page on which moderation should be changed
-
- level <0, 7>: Level of moderation,
- 1 - moderate page
- 2 - moderate children
- 4 - moderate descendants
- + conbinations
- """
- response = self.client.post("/admin/cms/page/%d/change-moderation/" % page.id, {'moderate': level})
- self.assertEquals(response.status_code, 200)
-
- def failUnlessWarns(self, category, message, f, *args, **kwargs):
- warningsShown = []
- result = _collectWarnings(warningsShown.append, f, *args, **kwargs)
-
- if not warningsShown:
- self.fail("No warnings emitted")
- first = warningsShown[0]
- for other in warningsShown[1:]:
- if ((other.message, other.category)
- != (first.message, first.category)):
- self.fail("Can't handle different warnings")
- self.assertEqual(first.message, message)
- self.assertTrue(first.category is category)
-
- return result
- assertWarns = failUnlessWarns
-
-
-class SettingsOverrideTestCase(CMSTestCase):
- settings_overrides = {}
-
- def _pre_setup(self):
- self._enter_settings_override()
- super(SettingsOverrideTestCase, self)._pre_setup()
-
- def _enter_settings_override(self):
- self._settings_ctx_manager = SettingsOverride(**self.settings_overrides)
- self._settings_ctx_manager.__enter__()
-
- def _post_teardown(self):
- super(SettingsOverrideTestCase, self)._post_teardown()
- self._exit_settings_override()
-
- def _exit_settings_override(self):
- self._settings_ctx_manager.__exit__(None, None, None)
+# -*- coding: utf-8 -*-
+from cms.admin.forms import save_permissions
+from cms.models import Title, Page
+from cms.models.moderatormodels import ACCESS_PAGE_AND_DESCENDANTS
+from cms.models.permissionmodels import PagePermission, PageUser
+from cms.models.pluginmodel import CMSPlugin
+from cms.plugins.text.models import Text
+from cms.test.util.context_managers import UserLoginContext, SettingsOverride
+from cms.utils.permissions import _thread_locals
+from django.conf import settings
+from django.contrib.auth.models import User, AnonymousUser
+from django.contrib.sites.models import Site
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.handlers.wsgi import WSGIRequest
+from django.core.urlresolvers import reverse
+from django.template.context import Context
+from django.template.defaultfilters import slugify
+from django.test.testcases import TestCase
+from menus.menu_pool import menu_pool
+from urlparse import urlparse
+import sys
+import urllib
+import warnings
+
+
+URL_CMS_PAGE = "/admin/cms/page/"
+URL_CMS_PAGE_ADD = URL_CMS_PAGE + "add/"
+URL_CMS_PAGE_CHANGE = URL_CMS_PAGE + "%d/"
+URL_CMS_PAGE_DELETE = URL_CMS_PAGE_CHANGE + "delete/"
+URL_CMS_PLUGIN_ADD = URL_CMS_PAGE + "add-plugin/"
+URL_CMS_PLUGIN_EDIT = URL_CMS_PAGE + "edit-plugin/"
+URL_CMS_PLUGIN_REMOVE = URL_CMS_PAGE + "remove-plugin/"
+URL_CMS_TRANSLATION_DELETE = URL_CMS_PAGE_CHANGE + "delete-translation/"
+
+class _Warning(object):
+ def __init__(self, message, category, filename, lineno):
+ self.message = message
+ self.category = category
+ self.filename = filename
+ self.lineno = lineno
+
+
+
+def _collectWarnings(observeWarning, f, *args, **kwargs):
+ def showWarning(message, category, filename, lineno, file=None, line=None):
+ assert isinstance(message, Warning)
+ observeWarning(_Warning(
+ message.args[0], category, filename, lineno))
+
+ # Disable the per-module cache for every module otherwise if the warning
+ # which the caller is expecting us to collect was already emitted it won't
+ # be re-emitted by the call to f which happens below.
+ for v in sys.modules.itervalues():
+ if v is not None:
+ try:
+ v.__warningregistry__ = None
+ except:
+ # Don't specify a particular exception type to handle in case
+ # some wacky object raises some wacky exception in response to
+ # the setattr attempt.
+ pass
+
+ origFilters = warnings.filters[:]
+ origShow = warnings.showwarning
+ warnings.simplefilter('always')
+ try:
+ warnings.showwarning = showWarning
+ result = f(*args, **kwargs)
+ finally:
+ warnings.filters[:] = origFilters
+ warnings.showwarning = origShow
+ return result
+
+class CMSTestCase(TestCase):
+ counter = 1
+
+ def _post_teardown(self):
+ # Needed to clean the menu keys cache, see menu.menu_pool.clear()
+ menu_pool.clear()
+ super(CMSTestCase, self)._post_teardown()
+
+ def login_user(self, user):
+ logged_in = self.client.login(username=user.username, password=user.username)
+ self.user = user
+ self.assertEqual(logged_in, True)
+
+ def login_user_context(self, user):
+ return UserLoginContext(self, user)
+
+ def get_superuser(self):
+ admin = User(username="admin", is_staff=True, is_active=True, is_superuser=True)
+ admin.set_password("admin")
+ admin.save()
+ return admin
+
+ def get_staff_user_with_no_permissions(self):
+ """
+ Used in security tests
+ """
+ staff = User(username="staff", is_staff=True, is_active=True)
+ staff.set_password("staff")
+ staff.save()
+ return staff
+
+ def get_new_page_data(self, parent_id=''):
+ page_data = {'title':'test page %d' % self.counter,
+ 'slug':'test-page-%d' % self.counter, 'language':settings.LANGUAGES[0][0],
+ 'site':1, 'template':'nav_playground.html', 'parent': parent_id}
+
+ # required only if user haves can_change_permission
+ page_data['pagepermission_set-TOTAL_FORMS'] = 0
+ page_data['pagepermission_set-INITIAL_FORMS'] = 0
+ page_data['pagepermission_set-MAX_NUM_FORMS'] = 0
+
+ self.counter = self.counter + 1
+ return page_data
+
+ def print_page_structure(self, title=None):
+ """Just a helper to see the page struct.
+ """
+ for page in Page.objects.drafts().order_by('tree_id', 'lft'):
+ print "%s%s #%d" % (" " * (page.level), page, page.id)
+
+ def print_node_structure(self, nodes, *extra):
+ def _rec(nodes, level=0):
+ ident = level * ' '
+ for node in nodes:
+ raw_attrs = [(bit, getattr(node, bit, node.attr.get(bit, "unknown"))) for bit in extra]
+ attrs = ', '.join(['%s: %r' % data for data in raw_attrs])
+ print "%s%s: %s" % (ident, node.title, attrs)
+ _rec(node.children, level+1)
+ _rec(nodes)
+
+ def assertObjectExist(self, qs, **filter):
+ try:
+ return qs.get(**filter)
+ except ObjectDoesNotExist:
+ pass
+ raise self.failureException, "ObjectDoesNotExist raised"
+
+ def assertObjectDoesNotExist(self, qs, **filter):
+ try:
+ qs.get(**filter)
+ except ObjectDoesNotExist:
+ return
+ raise self.failureException, "ObjectDoesNotExist not raised"
+
+ def create_page(self, parent_page=None, user=None, position="last-child",
+ title=None, site=1, published=False, in_navigation=False,
+ moderate=False, language=None, title_extra=None, **extra):
+ """
+ Common way for page creation with some checks
+ """
+ _thread_locals.user = user
+ if not language:
+ language = settings.LANGUAGES[0][0]
+ if settings.CMS_SITE_LANGUAGES.get(site, False):
+ language = settings.CMS_SITE_LANGUAGES[site][0]
+ site = Site.objects.get(pk=site)
+
+ page_data = {
+ 'site': site,
+ 'template': 'nav_playground.html',
+ 'published': published,
+ 'in_navigation': in_navigation,
+ }
+ if user:
+ page_data['created_by'] = user
+ page_data['changed_by'] = user
+ if parent_page:
+ page_data['parent'] = parent_page
+ page_data.update(extra)
+
+ page = Page(**page_data)
+ if parent_page:
+ page.insert_at(self.reload(parent_page), position)
+ page.save()
+
+ if settings.CMS_MODERATOR and user:
+ page.pagemoderator_set.create(user=user)
+
+ if not title:
+ title = 'test page %d' % self.counter
+ slug = 'test-page-%d' % self.counter
+ else:
+ slug = slugify(title)
+ self.counter = self.counter + 1
+ if not title_extra:
+ title_extra = {}
+ self.create_title(
+ title=title,
+ slug=slug,
+ language=language,
+ page=page,
+ **title_extra
+ )
+
+ del _thread_locals.user
+ return page
+
+ def create_title(self, title, slug, language, page, **extra):
+ return Title.objects.create(
+ title=title,
+ slug=slug,
+ language=language,
+ page=page,
+ **extra
+ )
+
+ def copy_page(self, page, target_page):
+ from cms.utils.page import get_available_slug
+
+ data = {
+ 'position': 'last-child',
+ 'target': target_page.pk,
+ 'site': 1,
+ 'copy_permissions': 'on',
+ 'copy_moderation': 'on',
+ }
+
+ response = self.client.post(URL_CMS_PAGE + "%d/copy-page/" % page.pk, data)
+ self.assertEquals(response.status_code, 200)
+ self.assertEquals(response.content, "ok")
+
+ title = page.title_set.all()[0]
+ copied_slug = get_available_slug(title)
+
+ copied_page = self.assertObjectExist(Page.objects, title_set__slug=copied_slug, parent=target_page)
+ return copied_page
+
+ def move_page(self, page, target_page, position="first-child"):
+ page.move_page(target_page, position)
+ return self.reload_page(page)
+
+ def reload_page(self, page):
+ """
+ Returns a fresh instance of the page from the database
+ """
+ return self.reload(page)
+
+ def reload(self, obj):
+ return obj.__class__.objects.get(pk=obj.pk)
+
+ def get_pages_root(self):
+ return urllib.unquote(reverse("pages-root"))
+
+ def get_context(self, path=None):
+ if not path:
+ path = self.get_pages_root()
+ context = {}
+ request = self.get_request(path)
+
+ context['request'] = request
+
+ return Context(context)
+
+ def get_request(self, path=None, language=settings.LANGUAGES[0][0]):
+ if not path:
+ path = self.get_pages_root()
+
+ parsed_path = urlparse(path)
+ host = parsed_path.netloc or 'testserver'
+ port = 80
+ if ':' in host:
+ host, port = host.split(':', 1)
+
+ environ = {
+ 'HTTP_COOKIE': self.client.cookies,
+ 'PATH_INFO': parsed_path.path,
+ 'QUERY_STRING': parsed_path.query,
+ 'REMOTE_ADDR': '127.0.0.1',
+ 'REQUEST_METHOD': 'GET',
+ 'SCRIPT_NAME': '',
+ 'SERVER_NAME': host,
+ 'SERVER_PORT': port,
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ 'wsgi.version': (1,0),
+ 'wsgi.url_scheme': 'http',
+ 'wsgi.errors': self.client.errors,
+ 'wsgi.multiprocess': True,
+ 'wsgi.multithread': False,
+ 'wsgi.run_once': False,
+ 'wsgi.input': ''
+ }
+ request = WSGIRequest(environ)
+ request.session = self.client.session
+ request.user = getattr(self, 'user', AnonymousUser())
+ request.LANGUAGE_CODE = language
+ return request
+
+ def create_page_user(self, username, password=None,
+ can_add_page=True, can_change_page=True, can_delete_page=True,
+ can_recover_page=True, can_add_pageuser=True, can_change_pageuser=True,
+ can_delete_pageuser=True, can_add_pagepermission=True,
+ can_change_pagepermission=True, can_delete_pagepermission=True,
+ grant_all=False):
+ """
+ Helper function for creating page user, through form on:
+ /admin/cms/pageuser/add/
+
+ Returns created user.
+ """
+ if grant_all:
+ return self.create_page_user(username, password,
+ True, True, True, True, True, True, True, True, True, True)
+
+ if password is None:
+ password=username
+
+ data = {
+ 'can_add_page': can_add_page,
+ 'can_change_page': can_change_page,
+ 'can_delete_page': can_delete_page,
+ 'can_recover_page': can_recover_page,
+ 'can_add_pageuser': can_add_pageuser,
+ 'can_change_pageuser': can_change_pageuser,
+ 'can_delete_pageuser': can_delete_pageuser,
+ 'can_add_pagepermission': can_add_pagepermission,
+ 'can_change_pagepermission': can_change_pagepermission,
+ 'can_delete_pagepermission': can_delete_pagepermission,
+ }
+ if hasattr(self, 'user'):
+ created_by = self.user
+ else:
+ created_by = User.objects.create_superuser('superuser', 'superuser@django-cms.org', 'superuser')
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ user = User.objects.create_user(username, 'username@django-cms.org', password)
+ user.is_staff = True
+ user.is_active = True
+ page_user = PageUser(created_by=created_by)
+ for field in [f.name for f in User._meta.local_fields]:
+ setattr(page_user, field, getattr(user, field))
+ user.save()
+ page_user.save()
+ save_permissions(data, page_user)
+ return user
+
+ def assign_user_to_page(self, page, user, grant_on=ACCESS_PAGE_AND_DESCENDANTS,
+ can_add=False, can_change=False, can_delete=False,
+ can_change_advanced_settings=False, can_publish=False,
+ can_change_permissions=False, can_move_page=False, can_moderate=False,
+ grant_all=False):
+ """Assigns given user to page, and gives him requested permissions.
+
+ Note: this is not happening over frontend, maybe a test for this in
+ future will be nice.
+ """
+ if grant_all:
+ return self.assign_user_to_page(page, user, grant_on,
+ True, True, True, True, True, True, True, True)
+
+ data = {
+ 'can_add': can_add,
+ 'can_change': can_change,
+ 'can_delete': can_delete,
+ 'can_change_advanced_settings': can_change_advanced_settings,
+ 'can_publish': can_publish,
+ 'can_change_permissions': can_change_permissions,
+ 'can_move_page': can_move_page,
+ 'can_moderate': can_moderate,
+ }
+
+ page_permission = PagePermission(page=page, user=user, grant_on=grant_on, **data)
+ page_permission.save()
+ return page_permission
+
+ def add_plugin(self, user=None, page=None, placeholder=None, language='en', body=''):
+ if not placeholder:
+ if page:
+ placeholder = page.placeholders.get(slot__iexact='Right-Column')
+ else:
+ placeholder = page.placeholders.get(slot__iexact='Right-Column')
+
+ plugin_base = CMSPlugin(
+ plugin_type='TextPlugin',
+ placeholder=placeholder,
+ position=1,
+ language=language
+ )
+ plugin_base.insert_at(None, position='last-child', commit=False)
+
+ plugin = Text(body=body)
+ plugin_base.set_base_attr(plugin)
+ plugin.save()
+ return plugin.pk
+
+ def publish_page(self, page, approve=False, user=None, published_check=True):
+ if user:
+ self.login_user(user)
+
+ if published_check and not approve:
+ # must have public object now
+ self.assertFalse(page.publisher_public)
+ self.assertFalse(page.published)
+
+ # publish / approve page by master
+ response = self.client.post(URL_CMS_PAGE + "%d/change-status/" % page.pk, {1 :1})
+ self.assertEqual(response.status_code, 200)
+
+ if not approve:
+ page = self.reload_page(page)
+ if published_check:
+ # must have public object now
+ self.assertTrue(page.publisher_public, "Page '%s' has no publisher_public" % page)
+ # and public object must be published
+ self.assertTrue(page.publisher_public.published)
+ return page
+
+ # approve
+ page = self.approve_page(page)
+ if published_check:
+ # must have public object now
+ self.assertTrue(page.publisher_public, "Page '%s' has no publisher_public" % page)
+ # and public object must be published
+ self.assertTrue(page.publisher_public.published)
+
+ return page
+
+ def approve_page(self, page):
+ response = self.client.get(URL_CMS_PAGE + "%d/approve/" % page.pk)
+ self.assertRedirects(response, URL_CMS_PAGE)
+ # reload page
+ return self.reload_page(page)
+
+ def check_published_page_attributes(self, page):
+ public_page = page.publisher_public
+
+ if page.parent:
+ self.assertEqual(page.parent_id, public_page.parent.publisher_draft.id)
+
+ self.assertEqual(page.level, public_page.level)
+
+ # TODO: add check for siblings
+
+ draft_siblings = list(page.get_siblings(True). \
+ filter(publisher_is_draft=True).order_by('tree_id', 'parent', 'lft'))
+ public_siblings = list(public_page.get_siblings(True). \
+ filter(publisher_is_draft=False).order_by('tree_id', 'parent', 'lft'))
+
+ skip = 0
+ for i, sibling in enumerate(draft_siblings):
+ if not sibling.publisher_public_id:
+ skip += 1
+ continue
+ self.assertEqual(sibling.id, public_siblings[i - skip].publisher_draft.id)
+
+ def request_moderation(self, page, level):
+ """Assign current logged in user to the moderators / change moderation
+
+ Args:
+ page: Page on which moderation should be changed
+
+ level <0, 7>: Level of moderation,
+ 1 - moderate page
+ 2 - moderate children
+ 4 - moderate descendants
+ + conbinations
+ """
+ response = self.client.post("/admin/cms/page/%d/change-moderation/" % page.id, {'moderate': level})
+ self.assertEquals(response.status_code, 200)
+
+ def failUnlessWarns(self, category, message, f, *args, **kwargs):
+ warningsShown = []
+ result = _collectWarnings(warningsShown.append, f, *args, **kwargs)
+
+ if not warningsShown:
+ self.fail("No warnings emitted")
+ first = warningsShown[0]
+ for other in warningsShown[1:]:
+ if ((other.message, other.category)
+ != (first.message, first.category)):
+ self.fail("Can't handle different warnings")
+ self.assertEqual(first.message, message)
+ self.assertTrue(first.category is category)
+
+ return result
+ assertWarns = failUnlessWarns
+
+
+class SettingsOverrideTestCase(CMSTestCase):
+ settings_overrides = {}
+
+ def _pre_setup(self):
+ self._enter_settings_override()
+ super(SettingsOverrideTestCase, self)._pre_setup()
+
+ def _enter_settings_override(self):
+ self._settings_ctx_manager = SettingsOverride(**self.settings_overrides)
+ self._settings_ctx_manager.__enter__()
+
+ def _post_teardown(self):
+ super(SettingsOverrideTestCase, self)._post_teardown()
+ self._exit_settings_override()
+
+ def _exit_settings_override(self):
+ self._settings_ctx_manager.__exit__(None, None, None)
View
23 cms/tests/__init__.py
@@ -2,26 +2,27 @@
from cms.tests.admin import AdminTestCase
from cms.tests.apphooks import ApphooksTestCase
from cms.tests.docs import DocsTestCase
-from cms.tests.menu import FixturesMenuTests, MenuTests, AdvancedSoftrootTests
+from cms.tests.forms import FormsTestCase
+from cms.tests.mail import MailTestCase
+from cms.tests.middleware import MiddlewareTestCase
+from cms.tests.multilingual import MultilingualTestCase
+from cms.tests.menu import FixturesMenuTests, MenuTests, AdvancedSoftrootTests, ShowSubMenuCheck
from cms.tests.navextender import NavExtenderTestCase
from cms.tests.nonroot import NonRootCase
from cms.tests.page import PagesTestCase, NoAdminPageTests
from cms.tests.permmod import PermissionModeratorTestCase
-from cms.tests.placeholder import PlaceholderTestCase, PlaceholderActionTests
-from cms.tests.placeholder import PlaceholderModelTests
+from cms.tests.placeholder import (PlaceholderModelTests, PlaceholderAdminTest,
+ PlaceholderTestCase, PlaceholderActionTests)
from cms.tests.plugins import PluginManyToManyTestCase, PluginsTestCase
+from cms.tests.publisher import PublisherTestCase
from cms.tests.rendering import RenderingTestCase
from cms.tests.reversion_tests import ReversionTestCase
-from cms.tests.site import SiteTestCase
-from cms.tests.urlutils import UrlutilsTestCase
-from cms.tests.publisher import PublisherTestCase
-from cms.tests.multilingual import MultilingualTestCase
-from cms.tests.mail import MailTestCase
from cms.tests.settings import SettingsTests
-from cms.tests.forms import FormsTestCase
+from cms.tests.site import SiteTestCase
from cms.tests.toolbar import ToolbarTests
-from cms.tests.middleware import MiddlewareTestCase
+from cms.tests.urlutils import UrlutilsTestCase
from cms.tests.views import ViewTests
+from cms.tests.security import SecurityTests
try:
from cms.tests.javascript import JavascriptTestCase
except ImportError:
@@ -29,4 +30,4 @@
import traceback
exc = traceback.format_exc()
warnings.warn("JavascriptTestCase cannot be run: %s" % exc)
-
+
View
27 cms/tests/menu.py
@@ -716,4 +716,29 @@ def test_02_top_in_nav(self):
AttributeObject(title='bbb', level=1, children=[])
])
]
- self.assertTreeQuality(soft_root, mock_tree, 'title', 'level')
+ self.assertTreeQuality(soft_root, mock_tree, 'title', 'level')
+
+
+class ShowSubMenuCheck(BaseMenuTest):
+ """
+ Tree from fixture:
+
+ + P1
+ | + P2
+ | + P3
+ + P4
+ | + P5
+ + P6
+ + P7 (not in menu)
+ + P8
+ """
+ fixtures = ['menus-sub.json']
+
+ def test_01_show_submenu(self):
+ context = self.get_context('/test-page-6/')
+ # test standard show_menu
+ tpl = Template("{% load menu_tags %}{% show_sub_menu %}")
+ tpl.render(context)
+ nodes = context['children']
+ self.assertEqual(len(nodes), 1)
+ self.assertEqual(nodes[0].id, 8)
View
15 cms/tests/multilingual.py
@@ -61,4 +61,17 @@ def test_02_multilingual_page(self):
public = page.publisher_public
placeholder = public.placeholders.all()[0]
self.assertEqual(placeholder.cmsplugin_set.filter(language='de').count(), 1)
- self.assertEqual(placeholder.cmsplugin_set.filter(language='en').count(), 1)
+ self.assertEqual(placeholder.cmsplugin_set.filter(language='en').count(), 1)
+
+ def test_03_multiple_reverse_monkeypatch(self):
+ """
+ This test is not very well behaved, every following
+ test that uses reverse will fail with a RuntimeException.
+ """
+ from cms.models import monkeypatch_reverse
+ monkeypatch_reverse()
+ monkeypatch_reverse()
+ try:
+ reverse('pages-root')
+ except RuntimeError:
+ self.fail('maximum recursion depth exceeded')
View
73 cms/tests/placeholder.py
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
+from __future__ import with_statement
from cms.exceptions import DuplicatePlaceholderWarning
from cms.models.placeholdermodel import Placeholder
from cms.test.testcases import CMSTestCase
+from cms.test.util.context_managers import SettingsOverride, UserLoginContext
from cms.utils.placeholder import PlaceholderNoAction, MLNGPlaceholderActions
from cms.utils.plugins import get_placeholders
from django.conf import settings
@@ -10,9 +12,9 @@
from django.core.urlresolvers import reverse
from django.template import TemplateSyntaxError, Template
from django.template.context import Context, RequestContext
-from testapp.fakemlng.models import Translations
-from testapp.placeholderapp.models import Example1, Example2, Example3, Example4, \
- Example5
+from testapp.fakemlng.models import Translations, Translations
+from testapp.placeholderapp.models import (Example1, Example2, Example3, Example4,
+ Example5, Example1, Example2, Example3, Example4, Example5)
class PlaceholderTestCase(CMSTestCase):
@@ -231,4 +233,67 @@ def test_04_excercise_get_attached_model(self):
def test_05_excercise_get_attached_field_name(self):
ph = Placeholder.objects.create(slot='test', default_width=300)
result = ph._get_attached_field_name()
- self.assertEqual(result, None) # Simple PH - no field name
+ self.assertEqual(result, None) # Simple PH - no field name
+
+
+class PlaceholderAdminTest(CMSTestCase):
+ placeholderconf = {'test': {
+ 'limits': {
+ 'global': 2,
+ 'TextPlugin': 1,
+ }
+ }
+ }
+ def get_placeholder(self):
+ return Placeholder.objects.create(slot='test')
+
+ def get_admin(self):
+ admin.autodiscover()
+ return admin.site._registry[Example1]
+
+ def get_post_request(self, data):
+ request = self.get_request()
+ request.POST._mutable = True
+ request.POST.update(data)
+ request.POST._mutable = False
+ request.method = 'POST'
+ request.environ['METHOD'] = 'POST'
+ return request
+
+ def test_01_test_global_limit(self):
+ placeholder = self.get_placeholder()
+ admin = self.get_admin()
+ data = {
+ 'plugin_type': 'LinkPlugin',
+ 'placeholder': placeholder.pk,
+ 'language': 'en',
+ }
+ superuser = self.get_superuser()
+ with UserLoginContext(self, superuser):
+ with SettingsOverride(CMS_PLACEHOLDER_CONF=self.placeholderconf):
+ request = self.get_post_request(data)
+ response = admin.add_plugin(request) # first
+ self.assertEqual(response.status_code, 200)
+ response = admin.add_plugin(request) # second
+ self.assertEqual(response.status_code, 200)
+ response = admin.add_plugin(request) # third
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.content, "This placeholder already has the maximum number of plugins.")
+
+ def test_02_test_type_limit(self):
+ placeholder = self.get_placeholder()
+ admin = self.get_admin()
+ data = {
+ 'plugin_type': 'TextPlugin',
+ 'placeholder': placeholder.pk,
+ 'language': 'en',
+ }
+ superuser = self.get_superuser()
+ with UserLoginContext(self, superuser):
+ with SettingsOverride(CMS_PLACEHOLDER_CONF=self.placeholderconf):
+ request = self.get_post_request(data)
+ response = admin.add_plugin(request) # first
+ self.assertEqual(response.status_code, 200)
+ response = admin.add_plugin(request) # second
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.content, "This placeholder already has the maximum number (1) of TextPlugin plugins.")
View
25 cms/tests/plugins.py
@@ -8,20 +8,23 @@
from cms.plugins.googlemap.models import GoogleMap
from cms.plugins.inherit.models import InheritPagePlaceholder
from cms.plugins.text.models import Text
-from cms.plugins.text.utils import plugin_tags_to_id_list, \
- plugin_tags_to_admin_html
-from cms.test.testcases import CMSTestCase, URL_CMS_PAGE, URL_CMS_PAGE_ADD, \
- URL_CMS_PLUGIN_ADD, URL_CMS_PLUGIN_EDIT, URL_CMS_PAGE_CHANGE, \
- URL_CMS_PLUGIN_REMOVE
+from cms.plugins.text.utils import (plugin_tags_to_id_list,
+ plugin_tags_to_admin_html)
+from cms.test.testcases import (CMSTestCase, URL_CMS_PAGE, URL_CMS_PAGE_ADD,
+ URL_CMS_PLUGIN_ADD, URL_CMS_PLUGIN_EDIT, URL_CMS_PAGE_CHANGE,
+ URL_CMS_PLUGIN_REMOVE)
+from cms.test.util.context_managers import SettingsOverride
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
+from django.core.urlresolvers import reverse
from django.forms.widgets import Media
from django.template import RequestContext
from testapp.pluginapp.models import Article, Section
from testapp.pluginapp.plugins.manytomany_rel.models import ArticlePluginModel
import os
+
@@ -83,8 +86,10 @@ def get_request(self, *args, **kwargs):
request = super(PluginsTestBaseCase, self).get_request(*args, **kwargs)
request.placeholder_media = Media()
return request
-
+
+
class PluginsTestCase(PluginsTestBaseCase):
+
def test_01_add_edit_plugin(self):
"""
@@ -287,12 +292,8 @@ def test_05_remove_plugin_not_associated_to_page(self):
'parent': int(response.content)
}
response = self.client.post(URL_CMS_PLUGIN_ADD, plugin_data)
-
- plugin_data = {
- 'plugin_id': int(response.content)
- }
- response = response.client.post(URL_CMS_PLUGIN_REMOVE, plugin_data)
- self.assertEquals(response.status_code, 200)
+ # no longer allowed for security reasons
+ self.assertEqual(response.status_code, 404)
def test_07_register_plugin_twice_should_raise(self):
number_of_plugins_before = len(plugin_pool.get_all_plugins())
View
175 cms/tests/security.py
@@ -0,0 +1,175 @@
+from cms.models.pluginmodel import CMSPlugin
+from cms.plugins.text.models import Text
+from cms.test.testcases import (CMSTestCase, URL_CMS_PLUGIN_ADD,
+ URL_CMS_PLUGIN_EDIT, URL_CMS_PLUGIN_REMOVE)
+from django.conf import settings
+from django.core.urlresolvers import reverse
+
+
+
+class SecurityTests(CMSTestCase):
+ """
+ Test security issues by trying some naive requests to add/alter/delete data.
+ """
+ def get_data(self):
+ page = self.create_page()
+ placeholder = page.placeholders.get(slot='body')
+ superuser = self.get_superuser()
+ staff = self.get_staff_user_with_no_permissions()
+ return page, placeholder, superuser, staff
+
+ def test_add(self):
+ """
+ Test adding a plugin to a *PAGE*.
+ """
+ page, placeholder, superuser, staff = self.get_data()
+ plugin_data = {
+ 'plugin_type':"TextPlugin",
+ 'language':settings.LANGUAGES[0][0],
+ 'placeholder':page.placeholders.get(slot="body").pk,
+ }
+ self.assertEqual(CMSPlugin.objects.count(), 0)
+ # log the user out and post the plugin data to the cms add-plugin URL.
+ self.client.logout()
+ response = self.client.post(URL_CMS_PLUGIN_ADD, plugin_data)
+ # since the user is not logged in, they should be prompted to log in.
+ self.assertTemplateUsed(response, 'admin/login.html')
+ self.assertEqual(CMSPlugin.objects.count(), 0)
+ # now log a staff user without permissions in and do the same as above.
+ self.client.login(username='staff', password='staff')
+ response = self.client.post(URL_CMS_PLUGIN_ADD, plugin_data)
+ # the user is logged in and the security check fails, so it should 404.
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(CMSPlugin.objects.count(), 0)
+
+ def test_edit(self):
+ """
+ Test editing a *PAGE* plugin
+ """
+ page, placeholder, superuser, staff = self.get_data()
+ # create the plugin using a superuser
+ plugin_id = self.add_plugin(superuser, page, placeholder, 'en', 'body')
+ plugin_data = {
+ 'plugin_id': plugin_id,
+ 'body': 'newbody',
+ }
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body') # check the body is as expected.
+ # log the user out, try to edit the plugin
+ self.client.logout()
+ url = URL_CMS_PLUGIN_EDIT + '%s/' % plugin.pk
+ response = self.client.post(url, plugin_data)
+ # since the user is not logged in, they should be prompted to log in.
+ self.assertTemplateUsed(response, 'admin/login.html')
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+ # now log a staff user without permissions in and do the same as above.
+ self.client.login(username='staff', password='staff')
+ response = self.client.post(url, plugin_data)
+ # the user is logged in and the security check fails, so it should 404.
+ self.assertEqual(response.status_code, 404)
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+
+ def test_delete(self):
+ """
+ Test deleting a *PAGE* plugin
+ """
+ page, placeholder, superuser, staff = self.get_data()
+ plugin_id = self.add_plugin(superuser, page, placeholder, 'en', 'body')
+ plugin_data = {
+ 'plugin_id': plugin_id,
+ }
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+ # log the user out, try to remove the plugin
+ self.client.logout()
+ response = self.client.post(URL_CMS_PLUGIN_REMOVE, plugin_data)
+ # since the user is not logged in, they should be prompted to log in.
+ self.assertTemplateUsed(response, 'admin/login.html')
+ self.assertEqual(CMSPlugin.objects.count(), 1)
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+ # now log a staff user without permissions in and do the same as above.
+ self.client.login(username='staff', password='staff')
+ response = self.client.post(URL_CMS_PLUGIN_REMOVE, plugin_data)
+ # the user is logged in and the security check fails, so it should 404.
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(CMSPlugin.objects.count(), 1)
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+
+ def test_add_ph(self):
+ """
+ Test adding a *NON PAGE* plugin
+ """
+ page, placeholder, superuser, staff = self.get_data()
+ plugin_data = {
+ 'plugin_type':"TextPlugin",
+ 'language':settings.LANGUAGES[0][0],
+ 'placeholder':page.placeholders.get(slot="body").pk,
+ }
+ url = reverse('admin:placeholderapp_example1_add_plugin')
+ self.assertEqual(CMSPlugin.objects.count(), 0)
+ # log the user out and try to add a plugin using PlaceholderAdmin
+ self.client.logout()
+ response = self.client.post(url, plugin_data)
+ # since the user is not logged in, they should be prompted to log in.
+ self.assertTemplateUsed(response, 'admin/login.html')
+ self.assertEqual(CMSPlugin.objects.count(), 0)
+ # now log a staff user without permissions in and do the same as above.
+ self.client.login(username='staff', password='staff')
+ response = self.client.post(url, plugin_data)
+ # the user is logged in and the security check fails, so it should 404.
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(CMSPlugin.objects.count(), 0)
+
+ def test_edit_ph(self):
+ """
+ Test editing a *NON PAGE* plugin
+ """
+ page, placeholder, superuser, staff = self.get_data()
+ plugin_id = self.add_plugin(superuser, page, placeholder, 'en', 'body')
+ url = reverse('admin:placeholderapp_example1_edit_plugin', args=(plugin_id,))
+ plugin_data = {
+ 'body': 'newbody',
+ 'language': 'en',
+ }
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+ # log the user out and try to edit a plugin using PlaceholderAdmin
+ self.client.logout()
+ response = self.client.post(url, plugin_data)
+ # since the user is not logged in, they should be prompted to log in.
+ self.assertTemplateUsed(response, 'admin/login.html')
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+ # now log a staff user without permissions in and do the same as above.
+ self.client.login(username='staff', password='staff')
+ response = self.client.post(url, plugin_data)
+ # the user is logged in and the security check fails, so it should 404.
+ self.assertEqual(response.status_code, 404)
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+
+ def test_delete_ph(self):
+ page, placeholder, superuser, staff = self.get_data()
+ plugin_id = self.add_plugin(superuser, page, placeholder, 'en', 'body')
+ plugin_data = {
+ 'plugin_id': plugin_id,
+ }
+ plugin = Text.objects.get(pk=plugin_id)
+ self.assertEqual(plugin.body, 'body')
+ url = reverse('admin:placeholderapp_example1_remove_plugin')
+ # log the user out and try to remove a plugin using PlaceholderAdmin
+ self.client.logout()
+ response = self.client.post(url, plugin_data)
+ # since the user is not logged in, they should be prompted to log in.
+ self.assertTemplateUsed(response, 'admin/login.html')
+ self.assertEqual(CMSPlugin.objects.count(), 1)
+ # now log a staff user without permissions in and do the same as above.
+ self.client.login(username='staff', password='staff')
+ response = self.client.post(url, plugin_data)
+ # the user is logged in and the security check fails, so it should 404.
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(CMSPlugin.objects.count(), 1)
View
1  cms/tests/toolbar.py
@@ -1,3 +1,4 @@
+from __future__ import with_statement
from cms.test.testcases import SettingsOverrideTestCase
from cms.test.util.context_managers import UserLoginContext
from django.conf import settings
View
1  cms/tests/views.py
@@ -1,3 +1,4 @@
+from __future__ import with_statement
from cms.apphook_pool import apphook_pool
from cms.test.testcases import SettingsOverrideTestCase
from cms.test.util.context_managers import SettingsOverride
View
9 menus/templatetags/menu_tags.py
@@ -22,8 +22,15 @@ def cut_after(node, levels, removed):
removed.extend(node.children)
node.children = []
else:
+ removed_local = []
for n in node.children:
- cut_after(n, levels - 1, removed)
+ if n.visible:
+ cut_after(n, levels - 1, removed)
+ else:
+ removed_local.append(n)
+ for n in removed_local:
+ node.children.remove(n)
+ removed.extend(removed_local)
def remove(node, removed):
removed.append(node)
View
1  setup.py
@@ -48,7 +48,6 @@
install_requires=[
'Django>=1.2',
'django-classy-tags>=0.2.2',
- 'PIL>=1.1.6',
'south>=0.7.2',
],
packages=find_packages(exclude=["example", "example.*","testdata","testdata.*"]),
View
1  tests/testapp/fixtures/menus-sub.json
@@ -0,0 +1 @@
+[{"pk": 1, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 2, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 3, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 4, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 5, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 6, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 7, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 8, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 9, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 10, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 11, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 12, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 13, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 14, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 15, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 16, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 17, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 18, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 19, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 20, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 21, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 22, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 23, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 24, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 25, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 26, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 27, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 28, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 29, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 30, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 31, "model": "cms.placeholder", "fields": {"slot": "right-column", "default_width": null}}, {"pk": 32, "model": "cms.placeholder", "fields": {"slot": "body", "default_width": null}}, {"pk": 1, "model": "cms.page", "fields": {"rght": 6, "navigation_extenders": null, "site": 1, "creation_date": "2011-01-22 17:07:09", "lft": 1, "in_navigation": true, "reverse_id": null, "login_required": false, "created_by": "script", "publication_end_date": null, "moderator_state": 0, "template": "nav_playground.html", "tree_id": 1, "placeholders": [1, 2], "limit_visibility_in_menu": null, "parent": null, "publisher_state": 1, "soft_root": false, "publication_date": "2011-01-22 17:07:09", "publisher_public": null, "level": 0, "changed_by": "script", "publisher_is_draft": true, "published": true}}, {"pk": 2, "model": "cms.page", "fields": {"rght": 5, "navigation_extenders": null, "site": 1, "creation_date": "2011-01-22 17:07:09", "lft": 2, "in_navigation": true, "reverse_id": null, "login_required": false, "created_by": "script", "publication_end_date": null, "moderator_state": 0, "template": "nav_playground.html", "tree_id": 1, "placeholders": [3, 4], "limit_visibility_in_menu": null, "parent": 1, "publisher_state": 1, "soft_root": false, "publication_date": "2011-01-22 17:07:09", "publisher_public": null, "level": 1, "changed_by": "script", "publisher_is_draft": true, "published": true}}, {"pk": 3, "model": "cms.page", "fields": {"rght": 4, "navigation_extenders": null, "site": 1, "creation_date": "2011-01-22 17:07:10", "lft": 3, "in_navigation": true, "reverse_id": null, "login_required": false, "created_by": "script", "publication_end_date": null, "moderator_state": 0, "template": "nav_playground.html", "tree_id": 1, "placeholders": [5, 6], "limit_visibility_in_menu": null, "parent": 2, "publisher_state": 1, "soft_root": false, "publication_date": "2011-01-22 17:07:10", "publisher_public": null, "level": 2, "changed_by": "script", "publisher_is_draft": true, "published": true}}, {"pk": 4, "model": "cms.page", "fields": {"rght": 4, "navigation_extenders": null, "site": 1, "creation_date": "2011-01-22 17:07:10", "lft": 1, "in_navigation": true, "reverse_id": null, "login_required": false, "created_by": "script", "publication_end_date": null, "moderator_state": 0, "template": "nav_playground.html", "tree_id": 2, "placeholders": [7, 8], "limit_visibility_in_menu": null, "parent": null, "publisher_state": 1, "soft_root": false, "publication_date": "2011-01-22 17:07:10", "publisher_public": null, "level": 0, "changed_by": "script", "publisher_is_draft": true, "published": true}}, {"pk": 5, "model": "cms.page", "fields": {"rght": 3, "navigation_extenders": null, "site": 1, "creation_date": "2011-01-22 17:07:11", "lft": 2, "in_navigation": true, "reverse_id": null, "login_required": false, "created_by": "script", "publication_end_date": null, "moderator_state": 0, "template": "nav_playground.html", "tree_id": 2, "placeholders": [9, 10], "limit_visibility_in_menu": null, "parent": 4, "publisher_state": 1, "soft_root": false, "publication_date": "2011-01-22 17:07:11", "publisher_public": null, "level": 1, "changed_by": "script", "publisher_is_draft": true, "published": true}}, {"pk": 6, "model": "cms.page", "fields": {"rght": 6, "navigation_extenders": null, "site": 1, "creation_date": "2011-01-22 17:07:12", "lft": 1, "in_navigation": true, "reverse_id": null, "login_required": false, "created_by": "script", "publication_end_date": null, "moderator_state": 0, "template": "nav_playground.html", "tree_id": 3, "placeholders": [11, 12], "limit_visibility_in_menu": null, "parent": null, "publisher_state": 1, "soft_root": false, "publication_date": "2011-01-22 17:07:12", "publisher_public": null, "level": 0, "changed_by": "admin", "publisher_is_draft": true, "published": true}}, {"pk": 7, "model": "cms.page", "fields": {"rght": 3, "navigation_extenders": null, "site": 1, "creation_date": "2011-01-22 17:07:12", "lft": 2, "in_navigation": false, "reverse_id": null, "login_required": false, "created_by": "script", "publication_end_date": null, "moderator_state": 0, "template": "nav_playground.html", "tree_id": 3, "placeholders": [13, 14], "limit_visibility_in_menu": null, "parent": 6, "publisher_state": 1, "soft_root": false, "publication_date": "2011-01-22 17:07:12", "publisher_public": null, "level": 1, "changed_by": "script", "publisher_is_draft": true, "published": true}}, {"pk": 8, "model": "cms.page", "fields": {"rght": 5, "navigation_extenders": null, "site": 1, "creation_date": "2011-01-22 17:07:13", "lft": 4, "in_navigation": true, "reverse_id": null, "login_required": false, "created_by": "script", "publication_end_date": null, "moderator_state": 0, "template": "nav_playground.html", "tree_id": 3, "placeholders": [15, 16], "limit_visibility_in_menu": null, "parent": 6, "publisher_state": 1, "soft_root": false, "publication_date": "2011-01-22 17:07:13", "publisher_public": null, "level": 1, "changed_by": "script", "publisher_is_draft": true, "published": true}}, {"pk": 1, "model": "cms.title", "fields": {"menu_title": null, "redirect": null, "meta_keywords": null, "page_title": null, "language": "en", "title": "test page 1", "has_url_overwrite": false, "application_urls": null, "creation_date": "2011-01-22 17:07:09", "page": 1, "path": "test-page-1", "meta_description": null, "slug": "test-page-1"}}, {"pk": 2, "model": "cms.title", "fields": {"menu_title": null, "redirect": null, "meta_keywords": null, "page_title": null, "language": "en", "title": "test page 2", "has_url_overwrite": false, "application_urls": null, "creation_date": "2011-01-22 17:07:10", "page": 2, "path": "test-page-1/test-page-2", "meta_description": null, "slug": "test-page-2"}}, {"pk": 3, "model": "cms.title", "fields": {"menu_title": null, "redirect": null, "meta_keywords": null, "page_title": null, "language": "en", "title": "test page 3", "has_url_overwrite": false, "application_urls": null, "creation_date": "2011-01-22 17:07:10", "page": 3, "path": "test-page-1/test-page-2/test-page-3", "meta_description": null, "slug": "test-page-3"}}, {"pk": 4, "model": "cms.title", "fields": {"menu_title": null, "redirect": null, "meta_keywords": null, "page_title": null, "language": "en", "title": "test page 4", "has_url_overwrite": false, "application_urls": null, "creation_date": "2011-01-22 17:07:11", "page": 4, "path": "test-page-4", "meta_description": null, "slug": "test-page-4"}}, {"pk": 5, "model": "cms.title", "fields": {"menu_title": null, "redirect": null, "meta_keywords": null, "page_title": null, "language": "en", "title": "test page 5", "has_url_overwrite": false, "application_urls": null, "creation_date": "2011-01-22 17:07:12", "page": 5, "path": "test-page-4/test-page-5", "meta_description": null, "slug": "test-page-5"}}, {"pk": 6, "model": "cms.title", "fields": {"menu_title": "", "redirect": "", "meta_keywords": "", "page_title": "", "language": "en", "title": "test page 6", "has_url_overwrite": false, "application_urls": "", "creation_date": "2011-01-22 17:07:12", "page": 6, "path": "test-page-6", "meta_description": "", "slug": "test-page-6"}}, {"pk": 7, "model": "cms.title", "fields": {"menu_title": null, "redirect": null, "meta_keywords": null, "page_title": null, "language": "en", "title": "test page 7", "has_url_overwrite": false, "application_urls": null, "creation_date": "2011-01-22 17:07:13", "page": 7, "path": "test-page-6/test-page-7", "meta_description": null, "slug": "test-page-7"}}, {"pk": 8, "model": "cms.title", "fields": {"menu_title": null, "redirect": null, "meta_keywords": null, "page_title": null, "language": "en", "title": "test page 8", "has_url_overwrite": false, "application_urls": null, "creation_date": "2011-01-22 17:07:13", "page": 8, "path": "test-page-6/test-page-8", "meta_description": null, "slug": "test-page-8"}}]
Please sign in to comment.
Something went wrong with that request. Please try again.