Repository: django-cms/django-cms

Issue:
Do not display the toolbar if the user does not have edit permissions
[claimed by @neo64bit]

Currently, if a user is logged in as a staff member, but does not have permission to change CMS pages, he still gets the toolbar. He can also see all the available templates. Changing the template results in an exception:

```
TypeError at /admin/cms/page/1/change_template/
sequence item 0: expected string, __proxy__ found
```



Discussion:
toolbar should still be there, but not the template dropdown


Resolving patch:
diff --git a/cms/cms_toolbar.py b/cms/cms_toolbar.py
--- a/cms/cms_toolbar.py
+++ b/cms/cms_toolbar.py
@@ -12,6 +12,8 @@
 from django.http import HttpResponseRedirect
 from django.utils.translation import ugettext_lazy as _
 import urllib
+from utils.permissions import has_page_change_permission, has_any_page_change_permissions
+from django.conf import settings
 
 
 def _get_page_admin_url(context, toolbar, **kwargs):
@@ -113,9 +115,15 @@ def get_items(self, context, **kwargs):
                     items.append(
                         GetButton(RIGHT, 'moderator', label, urlgetter)
                     )
-            
+
+                has_global_current_page_change_permission = False
+                if settings.CMS_PERMISSION:
+                    has_global_current_page_change_permission = has_page_change_permission(self.request)
+                has_current_page_change_permission = self.request.current_page.has_change_permission(self.request)
+
                 # The 'templates' Menu
-                items.append(self.get_template_menu(context, self.can_change, self.is_staff))
+                if has_global_current_page_change_permission or has_current_page_change_permission:
+                    items.append(self.get_template_menu(context, self.can_change, self.is_staff))
                 
                 # The 'page' Menu
                 items.append(self.get_page_menu(context, self.can_change, self.is_staff))


Test patch:
diff --git a/cms/test_utils/util/request_factory.py b/cms/test_utils/util/request_factory.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/util/request_factory.py
@@ -0,0 +1,175 @@
+from StringIO import StringIO
+from django.conf import settings
+from django.core.handlers.wsgi import WSGIRequest
+from django.http import SimpleCookie
+from django.test.client import (FakePayload, MULTIPART_CONTENT, encode_multipart, 
+    BOUNDARY, CONTENT_TYPE_RE)
+from django.utils.encoding import smart_str
+from urllib import urlencode
+from urlparse import urlparse
+import urllib
+
+
+class RequestFactory(object):
+    """
+    Class that lets you create mock Request objects for use in testing.
+
+    Usage:
+
+    rf = RequestFactory()
+    get_request = rf.get('/hello/')
+    post_request = rf.post('/submit/', {'foo': 'bar'})
+
+    Once you have a request object you can pass it to any view function,
+    just as if that view had been hooked up using a URLconf.
+    """
+    def __init__(self, **defaults):
+        self.defaults = defaults
+        self.cookies = SimpleCookie()
+        self.errors = StringIO()
+
+    def _base_environ(self, **request):
+        """
+        The base environment for a request.
+        """
+        environ = {
+            'HTTP_COOKIE':       self.cookies.output(header='', sep='; '),
+            'PATH_INFO':         '/',
+            'QUERY_STRING':      '',
+            'REMOTE_ADDR':       '127.0.0.1',
+            'REQUEST_METHOD':    'GET',
+            'SCRIPT_NAME':       '',
+            'SERVER_NAME':       'testserver',
+            'SERVER_PORT':       '80',
+            'SERVER_PROTOCOL':   'HTTP/1.1',
+            'wsgi.version':      (1,0),
+            'wsgi.url_scheme':   'http',
+            'wsgi.errors':       self.errors,
+            'wsgi.multiprocess': True,
+            'wsgi.multithread':  False,
+            'wsgi.run_once':     False,
+        }
+        environ.update(self.defaults)
+        environ.update(request)
+        return environ
+
+    def request(self, **request):
+        "Construct a generic request object."
+        req = WSGIRequest(self._base_environ(**request))
+        req.session = {}
+        return req
+
+    def _get_path(self, parsed):
+        # If there are parameters, add them
+        if parsed[3]:
+            return urllib.unquote(parsed[2] + ";" + parsed[3])
+        else:
+            return urllib.unquote(parsed[2])
+
+    def get(self, path, data={}, **extra):
+        "Construct a GET request"
+
+        parsed = urlparse(path)
+        r = {
+            'CONTENT_TYPE':    'text/html; charset=utf-8',
+            'PATH_INFO':       self._get_path(parsed),
+            'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
+            'REQUEST_METHOD': 'GET',
+            'wsgi.input':      FakePayload('')
+        }
+        r.update(extra)
+        return self.request(**r)
+
+    def post(self, path, data={}, content_type=MULTIPART_CONTENT,
+             **extra):
+        "Construct a POST request."
+
+        if content_type is MULTIPART_CONTENT:
+            post_data = encode_multipart(BOUNDARY, data)
+        else:
+            # Encode the content so that the byte representation is correct.
+            match = CONTENT_TYPE_RE.match(content_type)
+            if match:
+                charset = match.group(1)
+            else:
+                charset = settings.DEFAULT_CHARSET
+            post_data = smart_str(data, encoding=charset)
+
+        parsed = urlparse(path)
+        r = {
+            'CONTENT_LENGTH': len(post_data),
+            'CONTENT_TYPE':   content_type,
+            'PATH_INFO':      self._get_path(parsed),
+            'QUERY_STRING':   parsed[4],
+            'REQUEST_METHOD': 'POST',
+            'wsgi.input':     FakePayload(post_data),
+        }
+        r.update(extra)
+        return self.request(**r)
+
+    def head(self, path, data={}, **extra):
+        "Construct a HEAD request."
+
+        parsed = urlparse(path)
+        r = {
+            'CONTENT_TYPE':    'text/html; charset=utf-8',
+            'PATH_INFO':       self._get_path(parsed),
+            'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
+            'REQUEST_METHOD': 'HEAD',
+            'wsgi.input':      FakePayload('')
+        }
+        r.update(extra)
+        return self.request(**r)
+
+    def options(self, path, data={}, **extra):
+        "Constrict an OPTIONS request"
+
+        parsed = urlparse(path)
+        r = {
+            'PATH_INFO':       self._get_path(parsed),
+            'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
+            'REQUEST_METHOD': 'OPTIONS',
+            'wsgi.input':      FakePayload('')
+        }
+        r.update(extra)
+        return self.request(**r)
+
+    def put(self, path, data={}, content_type=MULTIPART_CONTENT,
+            **extra):
+        "Construct a PUT request."
+
+        if content_type is MULTIPART_CONTENT:
+            post_data = encode_multipart(BOUNDARY, data)
+        else:
+            post_data = data
+
+        # Make `data` into a querystring only if it's not already a string. If
+        # it is a string, we'll assume that the caller has already encoded it.
+        query_string = None
+        if not isinstance(data, basestring):
+            query_string = urlencode(data, doseq=True)
+
+        parsed = urlparse(path)
+        r = {
+            'CONTENT_LENGTH': len(post_data),
+            'CONTENT_TYPE':   content_type,
+            'PATH_INFO':      self._get_path(parsed),
+            'QUERY_STRING':   query_string or parsed[4],
+            'REQUEST_METHOD': 'PUT',
+            'wsgi.input':     FakePayload(post_data),
+        }
+        r.update(extra)
+        return self.request(**r)
+
+    def delete(self, path, data={}, **extra):
+        "Construct a DELETE request."
+
+        parsed = urlparse(path)
+        r = {
+            'PATH_INFO':       self._get_path(parsed),
+            'QUERY_STRING':    urlencode(data, doseq=True) or parsed[4],
+            'REQUEST_METHOD': 'DELETE',
+            'wsgi.input':      FakePayload('')
+        }
+        r.update(extra)
+        return self.request(**r)
\ No newline at end of file
diff --git a/cms/tests/toolbar.py b/cms/tests/toolbar.py
--- a/cms/tests/toolbar.py
+++ b/cms/tests/toolbar.py
@@ -2,6 +2,7 @@
 from cms.api import create_page
 from cms.cms_toolbar import CMSToolbar
 from cms.test_utils.testcases import SettingsOverrideTestCase
+from cms.test_utils.util.context_managers import SettingsOverride
 from cms.toolbar.items import (Anchor, TemplateHTML, Switcher, List, ListItem, 
     GetButton)
 from django.conf import settings
@@ -145,7 +146,7 @@ def test_toolbar_anon(self):
     def test_toolbar_staff(self):
         page = create_page('test', 'nav_playground.html', 'en', published=True)
         request = self.request_factory.get(page.get_absolute_url())
-        request.user = self.get_staff()
+        request.user = self.get_superuser()
         request.current_page = page
         toolbar = CMSToolbar(request)
 
@@ -153,6 +154,22 @@ def test_toolbar_staff(self):
         # Logo + edit-mode + templates + page-menu + admin-menu + logout
         self.assertEqual(len(items), 6)
 
+        # check templates
+        templates = items[2]
+        self.assertTrue(isinstance(templates, List))
+        self.assertEqual(len(templates.raw_items), len(settings.CMS_TEMPLATES))
+        base = reverse('admin:cms_page_change_template', args=(page.pk,))
+        for item, template in zip(templates.raw_items, settings.CMS_TEMPLATES):
+            self.assertEqual(item.url, '%s?template=%s' % (base, template[0]))
+
+        # normal staff without templates
+        request.user = self.get_staff()
+        request.session = {}
+        toolbar = CMSToolbar(request)
+
+        items = toolbar.get_items({})
+        # Logo + edit-mode + page-menu + admin-menu + logout
+        self.assertEqual(len(items), 5)
         # check the logo is there
         logo = items[0]
         self.assertTrue(isinstance(logo, Anchor))
@@ -162,16 +179,8 @@ def test_toolbar_staff(self):
         self.assertTrue(isinstance(edit, Switcher))
         self.assertFalse(toolbar.edit_mode)
 
-        # check templates
-        templates = items[2]
-        self.assertTrue(isinstance(templates, List))
-        self.assertEqual(len(templates.raw_items), len(settings.CMS_TEMPLATES))
-        base = reverse('admin:cms_page_change_template', args=(page.pk,))
-        for item, template in zip(templates.raw_items, settings.CMS_TEMPLATES):
-            self.assertEqual(item.url, '%s?template=%s' % (base, template[0]))
-
         # check page menu
-        pagemenu = items[3]
+        pagemenu = items[2]
         self.assertTrue(isinstance(pagemenu, List))
         self.assertEqual(len(pagemenu.raw_items), 4)
 
@@ -185,15 +194,27 @@ def test_toolbar_staff(self):
             reverse('admin:cms_page_delete', args=(page.pk,)))
 
         # check the admin-menu
-        admin = items[4]
+        admin = items[3]
         self.assertTrue(isinstance(admin, List))
         self.assertEqual(len(admin.raw_items), 1) # only the link to main admin
         self.assertTrue(isinstance(admin.raw_items[0], ListItem))
 
         # check the logout button
-        logout = items[5]
+        logout = items[4]
         self.assertTrue(isinstance(logout, GetButton))
         self.assertEqual(logout.url, '?cms-toolbar-logout')
+
+    def test_toolbar_template_change_permission(self):
+        with SettingsOverride(CMS_PERMISSIONS=True):
+            page = create_page('test', 'nav_playground.html', 'en', published=True)
+            request = self.request_factory.get(page.get_absolute_url())
+            request.user = self.get_staff()
+            request.current_page = page
+            SessionMiddleware().process_request(request)
+            request.session = {}
+            toolbar = CMSToolbar(request)
+            items = toolbar.get_items({})
+            self.assertEqual([item for item in items if item.css_class_suffix == 'templates'], [])
         
     def test_toolbar_markup(self):
         superuser = self.get_superuser()
@@ -252,14 +273,15 @@ def test_toolbar_moderate_button(self):
         request = self.request_factory.get(page.get_absolute_url() + '?edit')
         request.user = self.get_staff()
         request.current_page = page
+        request.session = {}
         toolbar = CMSToolbar(request)
 
         self.assertTrue(toolbar.edit_mode)
 
         items = toolbar.get_items({})
 
-        # Logo + edit-mode + moderate + templates + page-menu + admin-menu + logout
-        self.assertEqual(len(items), 7)
+        # Logo + edit-mode + moderate + page-menu + admin-menu + logout
+        self.assertEqual(len(items), 6)
 
         moderate = items[2]
         self.assertTrue(isinstance(moderate, GetButton))
@@ -275,12 +297,13 @@ def test_toolbar_no_moderate_button(self):
         page = create_page('test', 'nav_playground.html', 'en', published=True)
         request = self.request_factory.get(page.get_absolute_url() + '?edit')
         request.user = self.get_staff()
+        request.session = {}
         request.current_page = page
         toolbar = CMSToolbar(request)
 
         self.assertTrue(toolbar.edit_mode)
 
         items = toolbar.get_items({})
-        # Logo + edit-mode + templates + page-menu + admin-menu + logout
-        self.assertEqual(len(items), 6)
+        # Logo + edit-mode + page-menu + admin-menu + logout
+        self.assertEqual(len(items), 5)