Repository: django-cms/django-cms

Issue:
Toolbar extension no longer works
Hey,

In this commit e46853b1d6e3b16bf6dc5ac304aff83c5ac2ad94 we introduced a logic that relies on an apphook's views to be in the same app as the app's toolbar.

This works in under normal circumstances where both views and toolbar are in the same app, but if app A's views were not sufficient and so I create app B to extend the views of app A, then this code will no longer work (it won't set `is_current_app`), thus breaking backwards compatibility.

what was the reason to replace `resolve(self.request.path).app_name` ?



Discussion:
It was not reliable enough to catch all the possible case.
Apparently, the fix is stil not comprehensive enough
Would you create an example application in the cms test_utils to demonstrate this case?

Wouldn't an easier/more reliable (?) way be to check if the current URL is "below" the URL at which the app is hooked?

How about if the toolbar may declare any number of supported applications? An explicit approach should be the most robust. My gut feeling is that there are a very large number of corner cases we are not aware of, otherwise


Resolving patch:
diff --git a/cms/toolbar/toolbar.py b/cms/toolbar/toolbar.py
--- a/cms/toolbar/toolbar.py
+++ b/cms/toolbar/toolbar.py
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
-from cms.utils.conf import get_cms_setting
 from cms.constants import LEFT, REFRESH_PAGE
 from cms.models import UserSettings, Placeholder
 from cms.toolbar.items import Menu, ToolbarAPIMixin, ButtonList
 from cms.toolbar_pool import toolbar_pool
 from cms.utils import get_language_from_request
+from cms.utils.compat.dj import installed_apps
+from cms.utils.conf import get_cms_setting
 from cms.utils.i18n import force_language
 
 from django import forms
@@ -95,17 +96,17 @@ def __init__(self, request):
             except Resolver404:
                 self.app_name = ""
         toolbars = toolbar_pool.get_toolbars()
+        parts = self.app_name.split('.')
+        while parts:
+            path = '.'.join(parts)
+            if path in installed_apps():
+                self.app_name = path
+                break
+            parts.pop()
 
         self.toolbars = SortedDict()
-        app_key = ''
-        for key in toolbars:
-            app_name = ".".join(key.split(".")[:-2])
-            if (self.app_name and app_name and
-                    self.app_name.startswith(app_name) and
-                    len(key) > len(app_key)):
-                app_key = key
         for key in toolbars:
-            toolbar = toolbars[key](self.request, self, key == app_key, app_key)
+            toolbar = toolbars[key](self.request, self, toolbars[key].check_current_app(key, self.app_name), self.app_name)
             self.toolbars[key] = toolbar
 
     @property
diff --git a/cms/toolbar_base.py b/cms/toolbar_base.py
--- a/cms/toolbar_base.py
+++ b/cms/toolbar_base.py
@@ -6,6 +6,7 @@
 
 
 class CMSToolbar(object):
+    supported_apps = None
 
     def __init__(self, request, toolbar, is_current_app, app_path):
         self.request = request
@@ -26,3 +27,14 @@ def post_template_populate(self):
 
     def request_hook(self):
         pass
+
+    @classmethod
+    def check_current_app(cls, key, app_name):
+        if cls.supported_apps is None:
+            local_apps = ".".join(key.split(".")[:-2]),
+        else:
+            local_apps = cls.supported_apps
+        for local_app in local_apps:
+            if app_name and local_app and app_name.startswith(local_app):
+                return True
+        return False
\ No newline at end of file


Test patch:
diff --git a/cms/test_utils/project/extensionapp/cms_toolbar.py b/cms/test_utils/project/extensionapp/cms_toolbar.py
--- a/cms/test_utils/project/extensionapp/cms_toolbar.py
+++ b/cms/test_utils/project/extensionapp/cms_toolbar.py
@@ -49,6 +49,8 @@ def populate(self):
 
 @toolbar_pool.register
 class MyPageExtensionToolbar(CMSToolbar):
+    supported_apps = ('cms.test_utils.project.extensionapp.cms_toolbar', 'cms.test_utils.project.placeholderapp')
+
     def populate(self):
         # always use draft if we have a page
         self.page = get_page_draft(self.request.current_page)
diff --git a/cms/tests/apphooks.py b/cms/tests/apphooks.py
--- a/cms/tests/apphooks.py
+++ b/cms/tests/apphooks.py
@@ -516,7 +516,11 @@ def test_toolbar_current_app_namespace(self):
                 path = reverse('namespaced_app_ns:sample-exempt')
             request = self.get_request(path)
             toolbar = CMSToolbar(request)
+            self.assertEqual(toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbar.CategoryToolbar'].app_path,
+                             'cms.test_utils.project.sampleapp')
             self.assertTrue(toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbar.CategoryToolbar'].is_current_app)
+            self.assertEqual(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyTitleExtensionToolbar'].app_path,
+                             'cms.test_utils.project.sampleapp')
             self.assertFalse(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyTitleExtensionToolbar'].is_current_app)
 
     def test_toolbar_current_app_apphook_with_implicit_current_app(self):
@@ -526,7 +530,11 @@ def test_toolbar_current_app_apphook_with_implicit_current_app(self):
                 path = reverse('namespaced_app_ns:current-app')
             request = self.get_request(path)
             toolbar = CMSToolbar(request)
+            self.assertEqual(toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbar.CategoryToolbar'].app_path,
+                             'cms.test_utils.project.sampleapp')
             self.assertTrue(toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbar.CategoryToolbar'].is_current_app)
+            self.assertEqual(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyTitleExtensionToolbar'].app_path,
+                             'cms.test_utils.project.sampleapp')
             self.assertFalse(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyTitleExtensionToolbar'].is_current_app)
 
     def test_toolbar_no_namespace(self):
@@ -540,6 +548,26 @@ def test_toolbar_no_namespace(self):
             self.assertFalse(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyTitleExtensionToolbar'].is_current_app)
             self.assertTrue(toolbar.toolbars['cms.test_utils.project.placeholderapp.cms_toolbar.Example1Toolbar'].is_current_app)
 
+    def test_toolbar_multiple_supported_apps(self):
+        # Test with a basic application with no defined app_name and no namespace
+        with SettingsOverride(ROOT_URLCONF='cms.test_utils.project.placeholderapp_urls'):
+            self.create_base_structure(APP_NAME, 'en')
+            path = reverse('detail', kwargs={'id': 20})
+            request = self.get_request(path)
+            toolbar = CMSToolbar(request)
+            self.assertEqual(toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbar.CategoryToolbar'].app_path,
+                             'cms.test_utils.project.placeholderapp')
+            self.assertFalse(toolbar.toolbars['cms.test_utils.project.sampleapp.cms_toolbar.CategoryToolbar'].is_current_app)
+            self.assertEqual(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyTitleExtensionToolbar'].app_path,
+                             'cms.test_utils.project.placeholderapp')
+            self.assertFalse(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyTitleExtensionToolbar'].is_current_app)
+            self.assertEqual(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyPageExtensionToolbar'].app_path,
+                             'cms.test_utils.project.placeholderapp')
+            self.assertTrue(toolbar.toolbars['cms.test_utils.project.extensionapp.cms_toolbar.MyPageExtensionToolbar'].is_current_app)
+            self.assertEqual(toolbar.toolbars['cms.test_utils.project.placeholderapp.cms_toolbar.Example1Toolbar'].app_path,
+                             'cms.test_utils.project.placeholderapp')
+            self.assertTrue(toolbar.toolbars['cms.test_utils.project.placeholderapp.cms_toolbar.Example1Toolbar'].is_current_app)
+
     def test_toolbar_staff(self):
         # Test that the toolbar contains edito mode switcher if placeholders are available
         apphooks = (