Repository: openwisp/openwisp-network-topology (language: python)

Issue:
[feature] Switch to autocomplete filters and autocomplete organization fields
We have added support of multi-tenant autocomplete filters in https://github.com/openwisp/openwisp-users/pull/331 

- TopologyAdmin
    - Update organization filter to add autocomplete functionality 
- NodeAdmin
    - Update organization filter to add autocomplete functionality 
    - Create a new filter for topology which inherits from MultitenantRelatedOrgFilter
- LinkAdmin
    - Update organization filter to add autocomplete functionality 
    - Create a new filter for topology which inherits from MultitenantRelatedOrgFilter

Let's also change the organization fields of all the admin classes to use autocomplete fields instead of select fields, which do not scale when the number of organization in the system is high. I updated the description of the issue.

[feature] Switch to autocomplete filters and autocomplete organization fields
We have added support of multi-tenant autocomplete filters in https://github.com/openwisp/openwisp-users/pull/331 

- TopologyAdmin
    - Update organization filter to add autocomplete functionality 
- NodeAdmin
    - Update organization filter to add autocomplete functionality 
    - Create a new filter for topology which inherits from MultitenantRelatedOrgFilter
- LinkAdmin
    - Update organization filter to add autocomplete functionality 
    - Create a new filter for topology which inherits from MultitenantRelatedOrgFilter

Let's also change the organization fields of all the admin classes to use autocomplete fields instead of select fields, which do not scale when the number of organization in the system is high. I updated the description of the issue.



Discussion / hints:

Fix patch:
diff --git a/openwisp_network_topology/admin.py b/openwisp_network_topology/admin.py
index b79c9e04..5e30ff1c 100644
--- a/openwisp_network_topology/admin.py
+++ b/openwisp_network_topology/admin.py
@@ -79,7 +79,7 @@ class TopologyAdmin(
         'metric',
         'receive_url',
     ]
-    list_filter = ['parser', 'strategy', ('organization', MultitenantOrgFilter)]
+    list_filter = ['parser', 'strategy', MultitenantOrgFilter]
     search_fields = ['label', 'id']
     actions = ['update_selected', 'unpublish_selected', 'publish_selected']
     fields = [
@@ -254,6 +254,12 @@ def readonly_properties(self, obj):
     readonly_properties.short_description = _('Properties')
 
 
+class TopologyFilter(MultitenantRelatedOrgFilter):
+    field_name = 'topology'
+    parameter_name = 'topology_id'
+    title = _('topology')
+
+
 @admin.register(Node)
 class NodeAdmin(NodeLinkMixin, BaseAdmin):
     model = Node
@@ -262,10 +268,11 @@ class NodeAdmin(NodeLinkMixin, BaseAdmin):
     list_display = ['get_name', 'organization', 'topology', 'addresses']
     search_fields = ['addresses', 'label', 'properties']
     list_filter = [
-        ('organization', MultitenantOrgFilter),
-        ('topology', MultitenantRelatedOrgFilter),
+        (MultitenantOrgFilter),
+        (TopologyFilter),
     ]
     multitenant_shared_relations = ('topology',)
+    autocomplete_fields = ('topology',)
     fields = [
         'topology',
         'organization',
@@ -314,10 +321,11 @@ class LinkAdmin(NodeLinkMixin, BaseAdmin):
     ]
     list_filter = [
         'status',
-        ('organization', MultitenantOrgFilter),
-        ('topology', MultitenantRelatedOrgFilter),
+        (MultitenantOrgFilter),
+        (TopologyFilter),
     ]
     multitenant_shared_relations = ('topology', 'source', 'target')
+    autocomplete_fields = ('topology',)
     fields = [
         'topology',
         'organization',
diff --git a/requirements.txt b/requirements.txt
index 08d8d0e1..a1918744 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 django>=3.0,<4.1
-openwisp-users~=1.0.0
+openwisp-users @ https://github.com/openwisp/openwisp-users/tarball/master
 netdiff @ https://github.com/openwisp/netdiff/tarball/master
 jsonfield~=3.1.0
 django-flat-json-widget~=0.2.0


Test patch:
diff --git a/openwisp_network_topology/integrations/device/tests.py b/openwisp_network_topology/integrations/device/tests.py
index 059d5b52..bf4094ba 100644
--- a/openwisp_network_topology/integrations/device/tests.py
+++ b/openwisp_network_topology/integrations/device/tests.py
@@ -386,6 +386,8 @@ def test_shared_topology_org_devices(self):
 class TestMonitoringIntegration(Base, TransactionTestCase):
     @mock.patch('openwisp_monitoring.db.timeseries_db.create_database')
     @mock.patch('openwisp_monitoring.device.utils.manage_short_retention_policy')
+    @mock.patch('openwisp_monitoring.device.utils.manage_default_retention_policy')
+    @mock.patch('openwisp_monitoring.device.exportable._exportable_fields', [])
     @mock.patch('openwisp_notifications.types.NOTIFICATION_TYPES', dict())
     def test_monitoring_integration(self, *args):
         topology, device, cert = self._create_test_env(parser='netdiff.OpenvpnParser')
@@ -460,12 +462,12 @@ def setUp(self):
 
     def test_node_change_list_queries(self):
         path = reverse('{0}_node_changelist'.format(self.prefix))
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             self.client.get(path)
 
     def test_link_change_list_queries(self):
         path = reverse('{0}_link_changelist'.format(self.prefix))
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             self.client.get(path)
 
     def test_link_node_different_topology(self):
diff --git a/openwisp_network_topology/tests/test_admin.py b/openwisp_network_topology/tests/test_admin.py
index 6ad084ce..4e151339 100644
--- a/openwisp_network_topology/tests/test_admin.py
+++ b/openwisp_network_topology/tests/test_admin.py
@@ -166,7 +166,7 @@ def test_node_change_form(self):
 
     def test_node_change_list_queries(self):
         path = reverse('{0}_node_changelist'.format(self.prefix))
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             self.client.get(path)
 
     def test_link_change_list_queries(self):
@@ -175,7 +175,7 @@ def test_link_change_list_queries(self):
         n2 = self._create_node(label='node2org1', topology=t)
         self._create_link(topology=t, source=n1, target=n2)
         path = reverse('{0}_link_changelist'.format(self.prefix))
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             self.client.get(path)
 
     def test_link_change_form(self):
@@ -331,6 +331,14 @@ def _create_multitenancy_test_env(self):
         )
         return data
 
+    def _get_autocomplete_view_path(
+        self, app_label, model_name, field_name, path='admin:ow-auto-filter'
+    ):
+        return (
+            f'{reverse(path)}?app_label={app_label}'
+            f'&model_name={model_name}&field_name={field_name}'
+        )
+
     def test_topology_queryset(self):
         data = self._create_multitenancy_test_env()
         perm = Permission.objects.get_by_natural_key(
@@ -343,13 +351,14 @@ def test_topology_queryset(self):
             hidden=[data['t2'].label, data['org2'].name, data['t3_inactive'].label],
         )
 
-    def test_topology_organization_fk_queryset(self):
+    def test_topology_organization_fk_autocomplete_view(self):
         data = self._create_multitenancy_test_env()
         self._test_multitenant_admin(
-            url=reverse(f'admin:{self.app_label}_topology_add'),
+            url=self._get_autocomplete_view_path(
+                self.app_label, 'topology', 'organization'
+            ),
             visible=[data['org1'].name],
             hidden=[data['org2'].name, data['inactive']],
-            select_widget=True,
             administrator=True,
         )
 
@@ -388,36 +397,40 @@ def test_link_organization_fk_queryset(self):
         response = self.client.get(reverse(f'admin:{self.app_label}_link_add'))
         self.assertNotContains(response, 'organization_id')
 
-    def test_node_topology_fk_queryset(self):
+    def test_node_topology_fk_autocomplete_view(self):
         data = self._create_multitenancy_test_env()
         self._test_multitenant_admin(
-            url=reverse(f'admin:{self.app_label}_node_add'),
+            url=self._get_autocomplete_view_path(
+                self.app_label, 'node', 'topology', path='admin:autocomplete'
+            ),
             visible=[data['t1'].label],
             hidden=[data['t2'].label, data['t3_inactive'].label],
         )
 
-    def test_link_topology_fk_queryset(self):
+    def test_link_topology_fk_autocomplete_view(self):
         data = self._create_multitenancy_test_env()
         self._test_multitenant_admin(
-            url=reverse(f'admin:{self.app_label}_link_add'),
+            url=self._get_autocomplete_view_path(
+                self.app_label, 'link', 'topology', path='admin:autocomplete'
+            ),
             visible=[data['t1'].label],
             hidden=[data['t2'].label, data['t3_inactive'].label],
         )
 
-    def test_node_topology_filter(self):
+    def test_node_topology_autocomplete_filter(self):
         data = self._create_multitenancy_test_env()
         t_special = self._create_topology(label='special', organization=data['org1'])
         self._test_multitenant_admin(
-            url=reverse(f'admin:{self.app_label}_node_changelist'),
+            url=self._get_autocomplete_view_path(self.app_label, 'node', 'topology'),
             visible=[data['t1'].label, t_special.label],
             hidden=[data['t2'].label, data['t3_inactive'].label],
         )
 
-    def test_link_topology_filter(self):
+    def test_link_topology_autocomplete_filter(self):
         data = self._create_multitenancy_test_env()
         t_special = self._create_topology(label='special', organization=data['org1'])
         self._test_multitenant_admin(
-            url=reverse(f'admin:{self.app_label}_link_changelist'),
+            url=self._get_autocomplete_view_path(self.app_label, 'link', 'topology'),
             visible=[data['t1'].label, t_special.label],
             hidden=[data['t2'].label, data['t3_inactive'].label],
         )
diff --git a/requirements-test.txt b/requirements-test.txt
index 0c2b5d6f..9ccca809 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -3,5 +3,6 @@ pytest-asyncio~=0.14.0
 pytest-django~=4.5.2
 freezegun~=1.1.0
 # Needed to test integrations
-openwisp-monitoring~=1.0.0
-openwisp-utils[qa]~=1.0.1
+openwisp-monitoring @ https://github.com/openwisp/openwisp-monitoring/tarball/master
+openwisp-controller @ https://github.com/openwisp/openwisp-controller/tarball/master
+openwisp-utils[qa] @ https://github.com/openwisp/openwisp-utils/tarball/master
diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py
index 5823985d..bae5622c 100644
--- a/tests/openwisp2/settings.py
+++ b/tests/openwisp2/settings.py
@@ -44,6 +44,8 @@
     'openwisp_network_topology.integrations.device',
     'openwisp_users',
     # admin
+    'import_export',
+    'admin_auto_filters',
     'django.contrib.admin',
     # rest framework
     'rest_framework',