Repository: openwisp/openwisp-firmware-upgrader (language: python)

Issue:
[enhancement] Register menu groups
Must be done after implementing register menu group functionality https://github.com/openwisp/openwisp-utils/issues/189


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 24bfdbef..b7fcdb93 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,7 +48,7 @@ jobs:
 
       - name: Install python dependencies
         run: |
-          pip install -U pip wheel setuptools
+          pip install -U "pip==20.2.4" wheel setuptools
           pip install ${{ matrix.django-version }}
           pip install -U -r requirements-test.txt
 
diff --git a/openwisp_firmware_upgrader/apps.py b/openwisp_firmware_upgrader/apps.py
index 6912bb06..f18dc405 100644
--- a/openwisp_firmware_upgrader/apps.py
+++ b/openwisp_firmware_upgrader/apps.py
@@ -1,8 +1,8 @@
-from django.conf import settings
 from django.db.models.signals import post_save
 from django.utils.translation import gettext_lazy as _
-from swapper import load_model
+from swapper import get_model_name, load_model
 
+from openwisp_utils.admin_theme.menu import register_menu_group
 from openwisp_utils.api.apps import ApiAppConfig
 from openwisp_utils.utils import default_or_test
 
@@ -23,19 +23,37 @@ class FirmwareUpdaterConfig(ApiAppConfig):
 
     def ready(self, *args, **kwargs):
         super().ready(*args, **kwargs)
-        self.add_default_menu_items()
+        self.register_menu_groups()
         self.connect_device_signals()
 
-    def add_default_menu_items(self):
-        menu_setting = 'OPENWISP_DEFAULT_ADMIN_MENU_ITEMS'
-        items = [
-            {'model': f'{self.label}.Build'},
-        ]
-        if not hasattr(settings, menu_setting):  # pragma: no cover
-            setattr(settings, menu_setting, items)
-        else:
-            current_menu = getattr(settings, menu_setting)
-            current_menu += items
+    def register_menu_groups(self):
+        register_menu_group(
+            position=100,
+            config={
+                'label': _('Firmware'),
+                'items': {
+                    1: {
+                        'label': _('Builds'),
+                        'model': get_model_name(self.label, 'Build'),
+                        'name': 'changelist',
+                        'icon': 'ow-build',
+                    },
+                    2: {
+                        'label': _('Categories'),
+                        'model': get_model_name(self.label, 'Category'),
+                        'name': 'changelist',
+                        'icon': 'ow-category',
+                    },
+                    3: {
+                        'label': _('Mass Upgrade Operations'),
+                        'model': get_model_name(self.label, 'BatchUpgradeOperation'),
+                        'name': 'changelist',
+                        'icon': 'ow-mass-upgrade',
+                    },
+                },
+                'icon': 'ow-firmware',
+            },
+        )
 
     def connect_device_signals(self):
         DeviceConnection = load_model('connection', 'DeviceConnection')
diff --git a/setup.py b/setup.py
index 458d1f1e..afe26303 100755
--- a/setup.py
+++ b/setup.py
@@ -6,6 +6,10 @@
 
 from openwisp_firmware_upgrader import get_version
 
+# TODO: change this when next version of openwisp_controller is released
+controller = 'https://github.com/openwisp/openwisp-controller/tarball/master'
+# TODO: change this when next version of openwisp_utils is released
+utils = 'https://github.com/openwisp/openwisp-utils/tarball/master'
 if sys.argv[-1] == 'publish':
     # delete any *.pyc, *.pyo and __pycache__
     os.system('find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf')
@@ -35,8 +39,8 @@
     include_package_data=True,
     zip_safe=False,
     install_requires=[
-        'openwisp-controller~=0.8',
-        'openwisp-utils[rest]~=0.7.1',
+        f'openwisp-controller @ {controller}',
+        f'openwisp-utils[rest] @ {utils}',
         'django-private-storage~=2.2',
         'swapper~=1.1',
     ],


Test patch:
diff --git a/openwisp_firmware_upgrader/tests/test_admin.py b/openwisp_firmware_upgrader/tests/test_admin.py
index d72858e9..6e4c600d 100644
--- a/openwisp_firmware_upgrader/tests/test_admin.py
+++ b/openwisp_firmware_upgrader/tests/test_admin.py
@@ -253,6 +253,21 @@ def test_image_queryset_no_model_nor_device_firmware(self):
         self.assertIn(yuncore, form.fields['image'].queryset)
         self.assertNotIn(img_org2, form.fields['image'].queryset)
 
+    def test_admin_menu_groups(self):
+        # Test menu group (openwisp-utils menu group) for Build, Category,
+        # BatchUpgradeOperation models
+        self._login()
+        models = ['build', 'category', 'batchupgradeoperation']
+        response = self.client.get(reverse('admin:index'))
+        for model in models:
+            with self.subTest(f'test_admin_group_for_{model}_model'):
+                url = reverse(f'admin:{self.app_label}_{model}_changelist')
+                self.assertContains(response, f'<a class="mg-link" href="{url}">')
+        with self.subTest('test_firmware_group_is_registered'):
+            self.assertContains(
+                response, '<div class="mg-dropdown-label">Firmware </div>', html=True,
+            )
+
 
 _mock_updrade = 'openwisp_firmware_upgrader.upgraders.openwrt.OpenWrt.upgrade'
 _mock_connect = 'openwisp_controller.connection.models.DeviceConnection.connect'
diff --git a/requirements-test.txt b/requirements-test.txt
index bcde6cfe..7dee7564 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -5,4 +5,4 @@ flake8
 redis>=3.4.1,<4.0.0
 django-redis>=4.11.0,<5.0
 mock-ssh-server~=0.8.0
-openwisp-utils[qa]~=0.7.1
+openwisp-utils[qa] @ https://github.com/openwisp/openwisp-utils/tarball/master
diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py
index d2e5cf83..0fe1fbc6 100644
--- a/tests/openwisp2/settings.py
+++ b/tests/openwisp2/settings.py
@@ -121,7 +121,7 @@
                 'django.template.context_processors.request',
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
-                'openwisp_utils.admin_theme.context_processor.menu_items',
+                'openwisp_utils.admin_theme.context_processor.menu_groups',
                 'openwisp_notifications.context_processors.notification_api_settings',
             ],
         },