Skip to content

Commit 2875359

Browse files
committed
Merge pull request django-cms#3276 from vovanbo/feature/appresolver_improvements
Support of nested namespaces in URLconfs in hooked apps
2 parents 142c117 + 10510ff commit 2875359

File tree

6 files changed

+89
-23
lines changed

6 files changed

+89
-23
lines changed

cms/app_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ class CMSApp(object):
55
menus = []
66
app_name = None
77
permissions = True
8+
exclude_permissions = []

cms/appresolver.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import with_statement
33

4-
import sys
5-
64
from django.conf import settings
75
from django.conf.urls import patterns
86
from django.contrib.sites.models import Site
@@ -45,8 +43,9 @@ def applications_page_check(request, current_page=None, path=None):
4543
page_id = resolver.resolve_page_id(path)
4644
# yes, it is application page
4745
page = Page.objects.public().get(id=page_id)
48-
# If current page was matched, then we have some override for content
49-
# from cms, but keep current page. Otherwise return page to which was application assigned.
46+
# If current page was matched, then we have some override for
47+
# content from cms, but keep current page. Otherwise return page
48+
# to which was application assigned.
5049
return page
5150
except Resolver404:
5251
# Raised if the page is not managed by an apphook
@@ -86,20 +85,20 @@ def resolve_page_id(self, path):
8685
else:
8786
try:
8887
sub_match = pattern.resolve(new_path)
89-
except Resolver404:
90-
exc = sys.exc_info()[0]
91-
if 'tried' in exc.args[0]:
92-
tried.extend([[pattern] + t for t in exc.args[0]['tried']])
93-
elif 'path' in exc.args[0]:
94-
tried.extend([[pattern] + t for t in exc.args[0]['path']])
88+
except Resolver404 as e:
89+
tried_match = e.args[0].get('tried')
90+
if tried_match is not None:
91+
tried.extend([[pattern] + t for t in tried_match])
92+
else:
93+
tried.extend([pattern])
9594
else:
9695
if sub_match:
9796
return pattern.page_id
9897
tried.append(pattern.regex.pattern)
9998
raise Resolver404({'tried': tried, 'path': new_path})
10099

101100

102-
def recurse_patterns(path, pattern_list, page_id, default_args=None):
101+
def recurse_patterns(path, pattern_list, page_id, default_args=None, nested=False):
103102
"""
104103
Recurse over a list of to-be-hooked patterns for a given path prefix
105104
"""
@@ -109,7 +108,7 @@ def recurse_patterns(path, pattern_list, page_id, default_args=None):
109108
# make sure we don't get patterns that start with more than one '^'!
110109
app_pat = app_pat.lstrip('^')
111110
path = path.lstrip('^')
112-
regex = r'^%s%s' % (path, app_pat)
111+
regex = r'^%s%s' % (path, app_pat) if not nested else r'^%s' % (app_pat)
113112
if isinstance(pattern, RegexURLResolver):
114113
# this is an 'include', recurse!
115114
resolver = RegexURLResolver(regex, 'cms_appresolver',
@@ -120,7 +119,7 @@ def recurse_patterns(path, pattern_list, page_id, default_args=None):
120119
if default_args:
121120
args.update(default_args)
122121
# see lines 243 and 236 of urlresolvers.py to understand the next line
123-
resolver._urlconf_module = recurse_patterns(regex, pattern.url_patterns, page_id, args)
122+
resolver._urlconf_module = recurse_patterns(regex, pattern.url_patterns, page_id, args, nested=True)
124123
else:
125124
# Re-do the RegexURLPattern with the new regular expression
126125
args = pattern.default_args
@@ -133,14 +132,15 @@ def recurse_patterns(path, pattern_list, page_id, default_args=None):
133132
return newpatterns
134133

135134

136-
def _flatten_patterns(patterns):
137-
flat = []
135+
def _set_permissions(patterns, exclude_permissions):
138136
for pattern in patterns:
139137
if isinstance(pattern, RegexURLResolver):
140-
flat += _flatten_patterns(pattern.url_patterns)
138+
if pattern.namespace in exclude_permissions:
139+
continue
140+
_set_permissions(pattern.url_patterns, exclude_permissions)
141141
else:
142-
flat.append(pattern)
143-
return flat
142+
from cms.utils.decorators import cms_perms
143+
pattern._callback = cms_perms(pattern.callback)
144144

145145

146146
def get_app_urls(urls):
@@ -167,7 +167,6 @@ def get_patterns_for_title(path, title):
167167
path += '/'
168168
page_id = title.page.id
169169
url_patterns += recurse_patterns(path, pattern_list, page_id)
170-
url_patterns = _flatten_patterns(url_patterns)
171170
return url_patterns
172171

173172

@@ -228,9 +227,8 @@ def get_app_patterns():
228227
resolver = AppRegexURLResolver(r'', 'app_resolver', app_name=app_ns, namespace=inst_ns)
229228
resolver.page_id = page_id
230229
if app.permissions:
231-
from cms.utils.decorators import cms_perms
232-
for pat in current_patterns:
233-
pat._callback = cms_perms(pat.callback)
230+
_set_permissions(current_patterns, app.exclude_permissions)
231+
234232
extra_patterns = patterns('', *current_patterns)
235233
resolver.url_patterns_dict[lang] = extra_patterns
236234
app_patterns.append(resolver)

cms/test_utils/project/sampleapp/cms_app.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from cms.apphook_pool import apphook_pool
44
from django.utils.translation import ugettext_lazy as _
55

6+
67
class SampleApp(CMSApp):
78
name = _("Sample App")
89
urls = ["cms.test_utils.project.sampleapp.urls"]
@@ -12,6 +13,17 @@ class SampleApp(CMSApp):
1213
apphook_pool.register(SampleApp)
1314

1415

16+
class SampleAppWithExcludedPermissions(CMSApp):
17+
name = _("Sample App with excluded permissions")
18+
urls = [
19+
"cms.test_utils.project.sampleapp.urls_excluded"
20+
]
21+
permissions = True
22+
exclude_permissions = ['excluded']
23+
24+
apphook_pool.register(SampleAppWithExcludedPermissions)
25+
26+
1527
class SampleApp2(CMSApp):
1628
name = _("Sample App 2")
1729
urls = ["cms.test_utils.project.sampleapp.urls2"]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.conf.urls import patterns, url, include
2+
3+
"""
4+
Used in test_apphook_excluded_permissions
5+
"""
6+
7+
urlpatterns = patterns('cms.test_utils.project.sampleapp.views',
8+
url(r'^excluded/',
9+
include('cms.test_utils.project.sampleapp.urls_example', namespace="excluded", app_name='some_app')),
10+
url(r'^not_excluded/',
11+
include('cms.test_utils.project.sampleapp.urls_example', namespace="not_excluded", app_name='some_app')),
12+
)

cms/tests/apphooks.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def test_implicit_apphooks(self):
118118
apphook_pool.clear()
119119
hooks = apphook_pool.get_apphooks()
120120
app_names = [hook[0] for hook in hooks]
121-
self.assertEqual(len(hooks), 3)
121+
self.assertEqual(len(hooks), 4)
122122
self.assertIn(NS_APP_NAME, app_names)
123123
self.assertIn(APP_NAME, app_names)
124124
apphook_pool.clear()
@@ -200,8 +200,10 @@ def test_apphook_permissions(self):
200200

201201
with force_language("en"):
202202
path = reverse('sample-settings')
203+
203204
response = self.client.get(path)
204205
self.assertEqual(response.status_code, 200)
206+
205207
page = en_title.page.publisher_public
206208
page.login_required = True
207209
page.save()
@@ -211,6 +213,26 @@ def test_apphook_permissions(self):
211213
self.assertEqual(response.status_code, 302)
212214
apphook_pool.clear()
213215

216+
def test_apphooks_with_excluded_permissions(self):
217+
with SettingsOverride(ROOT_URLCONF='cms.test_utils.project.second_urls_for_apphook_tests'):
218+
en_title = self.create_base_structure('SampleAppWithExcludedPermissions', 'en')
219+
220+
with force_language("en"):
221+
excluded_path = reverse('excluded:example')
222+
not_excluded_path = reverse('not_excluded:example')
223+
224+
page = en_title.page.publisher_public
225+
page.login_required = True
226+
page.save()
227+
page.publish('en')
228+
229+
excluded_response = self.client.get(excluded_path)
230+
not_excluded_response = self.client.get(not_excluded_path)
231+
self.assertEqual(excluded_response.status_code, 200)
232+
self.assertEqual(not_excluded_response.status_code, 302)
233+
234+
apphook_pool.clear()
235+
214236
def test_get_page_for_apphook_on_preview_or_edit(self):
215237
with SettingsOverride(ROOT_URLCONF='cms.test_utils.project.urls_3'):
216238
if get_user_model().USERNAME_FIELD == 'email':

docs/extending_cms/app_integration.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,28 @@ Here is a simple example::
440440
...
441441

442442

443+
If you have your own permission check in your app, or just don't want to wrap some nested apps
444+
with CMS permission decorator, then use ``exclude_permissions`` property of apphook::
443445

446+
class SampleApp(CMSApp):
447+
name = _("Sample App")
448+
urls = ["project.sampleapp.urls"]
449+
permissions = True
450+
exclude_permissions = ["some_nested_app"]
451+
452+
453+
For example, django-oscar_ apphook integration needs to be used with exclude permissions of dashboard app,
454+
because it use `customizable access function`__. So, your apphook in this case will looks like this::
455+
456+
class OscarApp(CMSApp):
457+
name = _("Oscar")
458+
urls = [
459+
patterns('', *application.urls[0])
460+
]
461+
exclude_permissions = ['dashboard']
462+
463+
.. _django-oscar: https://github.com/tangentlabs/django-oscar
464+
.. __: https://github.com/tangentlabs/django-oscar/blob/0.7.2/oscar/apps/dashboard/nav.py#L57
444465

445466
Automatically restart server on apphook changes
446467
-----------------------------------------------

0 commit comments

Comments
 (0)