Repository: HybirdCorp/creme_crm

Issue:
Duplicated buttons
The configuration allows adding a button twice to a content type:

- On the button configuration page `/creme_config/button_menu/portal`
- Given a button that is not specific to a content type (Usable in the default configuration)
- Remove this button from the default configuration (if exists)
- Add this button to a content type (Contact for example)
- Add this button again to the default configuration
- The button is present twice on a Contact page.

version 2.2 alpha (master)


Discussion:

Resolving patch:
diff --git a/creme/creme_core/gui/button_menu.py b/creme/creme_core/gui/button_menu.py
--- a/creme/creme_core/gui/button_menu.py
+++ b/creme/creme_core/gui/button_menu.py
@@ -61,7 +61,8 @@ def get_ctypes(self) -> Sequence[Type[CremeEntity]]:
 
     def has_perm(self, context) -> bool:
         permission = self.permission
-        return context['request'].user.has_perm(permission) if permission else True
+        # return context['request'].user.has_perm(permission) if permission else True
+        return context['user'].has_perm(permission) if permission else True
 
     def ok_4_display(self, entity: CremeEntity) -> bool:
         """Can this button be displayed on this entity's detail-view ?
diff --git a/creme/creme_core/templatetags/creme_menu.py b/creme/creme_core/templatetags/creme_menu.py
--- a/creme/creme_core/templatetags/creme_menu.py
+++ b/creme/creme_core/templatetags/creme_menu.py
@@ -18,10 +18,12 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ################################################################################
 
+from collections import OrderedDict
+
 from django.db.models import Q
 from django.template import Library
 
-from ..gui.button_menu import button_registry
+from ..gui import button_menu
 from ..gui.menu import creme_menu
 from ..models import ButtonMenuItem
 
@@ -34,21 +36,29 @@ def menu_display(context):
 
 
 # TODO: rename template file (menu-buttons.html ? detailview-buttons.html ? menu/buttons.html ?)
-@register.inclusion_tag('creme_core/templatetags/menu_buttons.html', takes_context=True)
+@register.inclusion_tag(
+    'creme_core/templatetags/menu_buttons.html', takes_context=True,
+)
 def menu_buttons_display(context):
     entity = context['object']
-    bmi = ButtonMenuItem.objects.filter(Q(content_type=entity.entity_type) |
-                                        Q(content_type__isnull=True)
-                                       ) \
-                                .exclude(button_id='') \
-                                .order_by('order') \
-                                .values_list('button_id', flat=True)
+    bmi = ButtonMenuItem.objects.filter(
+        Q(content_type=entity.entity_type)
+        | Q(content_type__isnull=True)
+    ).exclude(
+        button_id='',
+    ).order_by(
+        'order',
+    ).values_list(
+        'button_id', flat=True,
+    )
 
     button_ctxt = context.flatten()
     # TODO: pass the registry in the context ?
-    context['buttons'] = [
-        button.render(button_ctxt)
-        for button in button_registry.get_buttons(bmi, entity)
-    ]
+    buttons = OrderedDict()
+
+    for button in button_menu.button_registry.get_buttons(bmi, entity):
+        buttons[button.id_] = button.render(button_ctxt)
+
+    context['buttons'] = [*buttons.values()]
 
     return context


Test patch:
diff --git a/creme/creme_core/tests/templatetags/test_creme_menu.py b/creme/creme_core/tests/templatetags/test_creme_menu.py
new file mode 100644
--- /dev/null
+++ b/creme/creme_core/tests/templatetags/test_creme_menu.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+
+from copy import deepcopy
+
+from bleach._vendor import html5lib
+from django.template import Context, Template
+from django.utils.safestring import mark_safe
+
+from creme.creme_core.gui import button_menu
+from creme.creme_core.gui.button_menu import Button
+from creme.creme_core.models import (
+    ButtonMenuItem,
+    FakeContact,
+    FakeOrganisation,
+)
+
+from ..base import CremeTestCase
+
+
+class _TestButton(Button):
+    action_id = 'creme_core-tests-dosomethingawesome1'
+
+    def render(self, context):
+        return mark_safe(
+            f' <a class="menu_button" data-action="{self.action_id}" href="#"></a>'
+        )
+
+
+class TestButton01(_TestButton):
+    id_ = Button.generate_id('creme_core', 'test_ttags_creme_menu01')
+    action_id = 'creme_core-tests-dosomethingawesome01'
+
+
+class TestButton02(_TestButton):
+    id_ = Button.generate_id('creme_core', 'test_ttags_creme_menu02')
+    action_id = 'creme_core-tests-dosomethingawesome02'
+
+
+class TestButton03(_TestButton):
+    id_ = Button.generate_id('creme_core', 'test_ttags_creme_menu03')
+    action_id = 'creme_core-tests-dosomethingawesome03'
+
+
+class TestButton04(_TestButton):
+    id_ = Button.generate_id('creme_core', 'test_ttags_creme_menu04')
+    action_id = 'creme_core-tests-dosomethingawesome04'
+
+
+class TestButton05(_TestButton):
+    id_ = Button.generate_id('creme_core', 'test_ttags_creme_menu05')
+    action_id = 'creme_core-tests-dosomethingawesome05'
+
+
+# TODO: test menu_display()
+class CremeMenuTagsTestCase(CremeTestCase):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+
+        cls._original_button_registry = button_menu.button_registry
+        cls.button_registry = button_registry = deepcopy(button_menu.button_registry)
+        button_registry.register(
+            TestButton01,
+            TestButton02,
+            TestButton03,
+            TestButton04,
+            TestButton05,
+        )
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+
+        button_menu.brick_registry = cls._original_button_registry
+
+    def setUp(self):
+        super().setUp()
+        button_menu.button_registry = self.button_registry = deepcopy(self.button_registry)
+
+    def get_html_tree(self, content):
+        return html5lib.parse(content, namespaceHTMLElements=False)
+
+    def get_button_nodes(self, tree):
+        for li_node in tree.findall('.//li'):
+            for node in li_node.findall('.//a'):
+                if 'menu_button' in node.attrib.get('class').split():
+                    yield node
+
+    def test_menu_buttons_display01(self):
+        user = self.create_user()
+        orga = FakeOrganisation.objects.create(user=user, name='Nerv')
+
+        create_button = ButtonMenuItem.objects.create_if_needed
+        create_button(pk='test-button1', button=TestButton01, order=1)
+        create_button(pk='test-button2', button=TestButton02, order=102, model=FakeOrganisation)
+        create_button(pk='test-button3', button=TestButton03, order=101, model=FakeOrganisation)
+        create_button(pk='test-button4', button=TestButton04, order=102, model=FakeContact)
+
+        with self.assertNoException():
+            template = Template(
+                r'{% load creme_menu %}'
+                r'{% menu_buttons_display %}'
+            )
+            render = template.render(Context({'user': user, 'object': orga}))
+
+        # print(render)
+        data_actions = {
+            node.attrib.get('data-action'): i
+            for i, node in enumerate(self.get_button_nodes(self.get_html_tree(render)))
+        }
+
+        def assertInButtons(action_id):
+            index = data_actions.get(action_id)
+            if index is None:
+                self.fail(f'action_id={action_id} not found in {data_actions.keys()}')
+
+            return index
+
+        index1 = assertInButtons(TestButton01.action_id)
+        index2 = assertInButtons(TestButton02.action_id)
+        index3 = assertInButtons(TestButton03.action_id)
+        self.assertLess(index1, index2)
+        self.assertLess(index1, index3)
+        self.assertLess(index3, index2)
+        self.assertNotIn(TestButton04.action_id, data_actions)
+        self.assertNotIn(TestButton05.action_id, data_actions)
+
+    def test_menu_buttons_display02(self):
+        "A button present in default config & ContentType's one => not duplicated."
+        user = self.create_user()
+        orga = FakeOrganisation.objects.create(user=user, name='Nerv')
+
+        create_button = ButtonMenuItem.objects.create_if_needed
+        create_button(pk='test-button1', button=TestButton01, order=1)
+        create_button(pk='test-button2', button=TestButton02, order=101, model=FakeOrganisation)
+        create_button(pk='test-button3', button=TestButton01, order=102, model=FakeOrganisation)
+
+        with self.assertNoException():
+            template = Template(
+                r'{% load creme_menu %}'
+                r'{% menu_buttons_display %}'
+            )
+            render = template.render(Context({'user': user, 'object': orga}))
+
+        # print(render)
+        actions_ids = []
+        for node in self.get_button_nodes(self.get_html_tree(render)):
+            action_id = node.attrib.get('data-action')
+            if action_id in (TestButton01.action_id, TestButton02.action_id):
+                actions_ids.append(action_id)
+
+        self.assertListEqual(
+            [TestButton01.action_id, TestButton02.action_id],
+            actions_ids,
+        )