From 4c892a2518e5dc835adf47826e434e4b632828dd Mon Sep 17 00:00:00 2001 From: niteshsinha17 Date: Thu, 4 Mar 2021 18:55:35 +0530 Subject: [PATCH 1/4] [enhancement] Show only relevant templates in device admin #345 closes #345 --- openwisp_controller/config/admin.py | 6 +++ .../static/config/js/default_templates.js | 46 ++++++++++++++++++- openwisp_controller/config/utils.py | 15 ++++++ openwisp_controller/config/views.py | 24 +++++++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/openwisp_controller/config/admin.py b/openwisp_controller/config/admin.py index 5777540ce..35b2fbc73 100644 --- a/openwisp_controller/config/admin.py +++ b/openwisp_controller/config/admin.py @@ -24,6 +24,7 @@ from openwisp_controller.config.views import ( get_default_templates, + get_relevant_templates, get_template_default_values, ) from openwisp_users.models import Organization @@ -485,6 +486,11 @@ def get_urls(self): get_default_templates, name='get_default_templates', ), + url( + r'^config/get-relevant-templates/(?P[^/]+)/$', + get_relevant_templates, + name='get_relavant_templates', + ), url( r'^get-template-default-values/$', get_template_default_values, diff --git a/openwisp_controller/config/static/config/js/default_templates.js b/openwisp_controller/config/static/config/js/default_templates.js index d7bf4286e..ad1f3bfcf 100644 --- a/openwisp_controller/config/static/config/js/default_templates.js +++ b/openwisp_controller/config/static/config/js/default_templates.js @@ -7,6 +7,7 @@ django.jQuery(function ($) { // ensures getDefaultTemplates execute only after other // onChange event handlers attached this field has been // executed. + getRelevantTemplates(); getDefaultTemplates(urls); }); }); @@ -27,7 +28,7 @@ django.jQuery(function ($) { return; } var url = urls[orgID], - isNew = $('#id_config-0-id').length == 0; + isNew = $('#id_config-0-id').length === 0; // if device is not new, do not execute on page load if (!isNew && firstRun) { return; @@ -46,10 +47,49 @@ django.jQuery(function ($) { }); }); }, + getRelevantTemplates = function () { + var orgID = $('#id_organization').val(), + backend = $('#id_config-0-backend').val(); + if (!orgID || !backend) { + return; + } + // proceed only if an organization and a backend have been selected + if (orgID.length === 0 || backend.length === 0) { + return; + } + var url = '/admin/config/device/config/get-relevant-templates/'; + // get relevant templates of selected org and backend + url = url + orgID + '/' + '?backend=' + backend; + $.get(url).done(function (data) { + var relevantTemplates = {}; + $.each(data.templates, function (i, uuid) { + relevantTemplates[uuid] = uuid; + }); + $('input.sortedm2m').each(function () { + if (($(this).val() in relevantTemplates) === false) { + // hide templates which are not relevant + $(this).parent().parent().hide(); + } else { + // show templates which are relevant + $(this).parent().parent().show(); + } + }); + // change help text if no relevant template + var msg = "Choose items and order by drag & drop."; + if (Object.keys(relevantTemplates).length === 0) { + msg = "No Template available"; + } + if (gettext) { + msg = gettext(msg); + } + $('.sortedm2m-container > .help').text(msg); + }); + }, bindDefaultTemplateLoading = function (urls) { var backendField = $('#id_config-0-backend'); $('#id_organization').change(function () { if ($('#id_config-0-backend').length > 0) { + getRelevantTemplates(); getDefaultTemplates(urls); } }); @@ -59,6 +99,7 @@ django.jQuery(function ($) { $('#config-group > fieldset.module').ready(function () { $('div.add-row > a').click(function () { addChangeEventToBackend(urls); + getRelevantTemplates(); getDefaultTemplates(urls); }); }); @@ -66,4 +107,7 @@ django.jQuery(function ($) { firstRun = false; }; window.bindDefaultTemplateLoading = bindDefaultTemplateLoading; + $(document).ready(function () { + getRelevantTemplates(); + }); }); diff --git a/openwisp_controller/config/utils.py b/openwisp_controller/config/utils.py index 8be4a0546..e191e39b8 100644 --- a/openwisp_controller/config/utils.py +++ b/openwisp_controller/config/utils.py @@ -199,3 +199,18 @@ def get_default_templates_queryset( if backend: queryset = queryset.filter(backend=backend) return queryset + + +def get_relevant_templates_queryset(organization_id, backend, model=None): + """ + It filters template as: + * Shared templates (organization==None), belonging to the + same config backend of the device. + * Templates belonging to the same organization and config backend + of the device + """ + queryset = model.objects.filter(backend=backend) + queryset = queryset.filter( + Q(organization_id=organization_id) | Q(organization_id=None) + ) + return queryset diff --git a/openwisp_controller/config/views.py b/openwisp_controller/config/views.py index f73627d53..614732d20 100644 --- a/openwisp_controller/config/views.py +++ b/openwisp_controller/config/views.py @@ -12,7 +12,11 @@ from openwisp_users.models import Organization from .settings import BACKENDS, VPN_BACKENDS -from .utils import get_default_templates_queryset, get_object_or_404 +from .utils import ( + get_default_templates_queryset, + get_object_or_404, + get_relevant_templates_queryset, +) Template = load_model('config', 'Template') @@ -34,6 +38,24 @@ def get_default_templates(request, organization_id): return JsonResponse({'default_templates': uuids}) +def get_relevant_templates(request, organization_id): + """ + returns relevant templates of specified organization + and config backend + """ + backend = request.GET.get("backend", None) + user = request.user + authenticated = user.is_authenticated + if not authenticated and not user.is_staff: + return HttpResponse(status=403) + org = get_object_or_404(Organization, pk=organization_id, is_active=True) + templates = get_relevant_templates_queryset(org.pk, backend, model=Template).only( + 'id' + ) + uuids = [str(t.pk) for t in templates] + return JsonResponse({'templates': uuids}) + + ALL_BACKENDS = BACKENDS + VPN_BACKENDS # ``available_schemas`` and ``available_schemas_json`` From c550f396e23a45785c2e90ded16ef7c1f3d9160f Mon Sep 17 00:00:00 2001 From: niteshsinha17 Date: Sun, 7 Mar 2021 23:25:51 +0530 Subject: [PATCH 2/4] [chores] Added test --- openwisp_controller/config/admin.py | 2 +- .../static/config/js/default_templates.js | 3 ++ .../config/tests/test_views.py | 40 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/openwisp_controller/config/admin.py b/openwisp_controller/config/admin.py index 35b2fbc73..a2d146f94 100644 --- a/openwisp_controller/config/admin.py +++ b/openwisp_controller/config/admin.py @@ -489,7 +489,7 @@ def get_urls(self): url( r'^config/get-relevant-templates/(?P[^/]+)/$', get_relevant_templates, - name='get_relavant_templates', + name='get_relevant_templates', ), url( r'^get-template-default-values/$', diff --git a/openwisp_controller/config/static/config/js/default_templates.js b/openwisp_controller/config/static/config/js/default_templates.js index ad1f3bfcf..cd7aafa4f 100644 --- a/openwisp_controller/config/static/config/js/default_templates.js +++ b/openwisp_controller/config/static/config/js/default_templates.js @@ -68,6 +68,9 @@ django.jQuery(function ($) { $('input.sortedm2m').each(function () { if (($(this).val() in relevantTemplates) === false) { // hide templates which are not relevant + // also removed disabled attribute and uncheck it + $(this).prop("disabled", false); + $(this).prop("checked", false); $(this).parent().parent().hide(); } else { // show templates which are relevant diff --git a/openwisp_controller/config/tests/test_views.py b/openwisp_controller/config/tests/test_views.py index 8a61297f8..0890b591b 100644 --- a/openwisp_controller/config/tests/test_views.py +++ b/openwisp_controller/config/tests/test_views.py @@ -141,3 +141,43 @@ def test_get_default_templates_400(self): reverse('admin:get_default_templates', args=['wrong']) ) self.assertEqual(response.status_code, 404) + + def test_get_relevant_templates(self): + org1 = self._create_org(name='org1') + org2 = self._create_org(name='org2') + t1 = self._create_template( + name='t1', organization=org1, backend='netjsonconfig.OpenWrt' + ) + _ = self._create_template( + name='t2', organization=org2, backend='netjsonconfig.OpenWrt' + ) + # shared template + t3 = self._create_template( + organization=None, name='t3', default=True, backend='netjsonconfig.OpenWrt' + ) + self._login() + + r = self.client.get( + reverse('admin:get_relevant_templates', args=[org1.pk]), + {'backend': 'netjsonconfig.OpenWrt'}, + ) + templates = r.json()['templates'] + self.assertEqual(len(templates), 2) + self.assertIn(str(t1.pk), templates) + self.assertIn(str(t3.pk), templates) + + def test_get_relevant_templates_403(self): + org1 = self._create_org(name='org1') + response = self.client.get( + reverse('admin:get_relevant_templates', args=[org1.pk]), + {'backend': 'netjsonconfig.OpenWrt'}, + ) + self.assertEqual(response.status_code, 403) + + def test_get_relevant_templates_404(self): + self._login() + response = self.client.get( + reverse('admin:get_relevant_templates', args=['wrong']), + {'backend': 'netjsonconfig.OpenWrt'}, + ) + self.assertEqual(response.status_code, 404) From a5d609950998727fd554a8029bb1e3b02a5fa7b7 Mon Sep 17 00:00:00 2001 From: niteshsinha17 Date: Fri, 12 Mar 2021 13:08:03 +0530 Subject: [PATCH 3/4] [fix] Fixed disabled template issue --- openwisp_controller/config/admin.py | 12 +++++ .../static/config/js/default_templates.js | 54 ++++++++++++------- .../templates/admin/config/change_form.html | 5 +- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/openwisp_controller/config/admin.py b/openwisp_controller/config/admin.py index a2d146f94..b634b5fdc 100644 --- a/openwisp_controller/config/admin.py +++ b/openwisp_controller/config/admin.py @@ -479,6 +479,17 @@ def _get_default_template_urls(self): urls[str(org.pk)] = reverse('admin:get_default_templates', args=[org.pk]) return json.dumps(urls) + def _get_relevant_template_urls(self): + """ + returns URLs to get relevant templates + used in change_form.html template + """ + organizations = Organization.active.all() + urls = {} + for org in organizations: + urls[str(org.pk)] = reverse('admin:get_relevant_templates', args=[org.pk]) + return json.dumps(urls) + def get_urls(self): return [ url( @@ -501,6 +512,7 @@ def get_urls(self): def get_extra_context(self, pk=None): ctx = super().get_extra_context(pk) ctx.update({'default_template_urls': self._get_default_template_urls()}) + ctx.update({'relevant_template_urls': self._get_relevant_template_urls()}) return ctx def add_view(self, request, form_url='', extra_context=None): diff --git a/openwisp_controller/config/static/config/js/default_templates.js b/openwisp_controller/config/static/config/js/default_templates.js index cd7aafa4f..8e519303f 100644 --- a/openwisp_controller/config/static/config/js/default_templates.js +++ b/openwisp_controller/config/static/config/js/default_templates.js @@ -1,14 +1,15 @@ 'use strict'; django.jQuery(function ($) { var firstRun = true, - addChangeEventToBackend = function (urls) { + addChangeEventToBackend = function (default_urls, relevant_urls) { + console.log(default_urls,relevant_urls); $('#id_config-0-backend').change(function () { setTimeout(function () { // ensures getDefaultTemplates execute only after other // onChange event handlers attached this field has been // executed. - getRelevantTemplates(); - getDefaultTemplates(urls); + getRelevantTemplates(relevant_urls); + getDefaultTemplates(default_urls); }); }); }, @@ -20,6 +21,7 @@ django.jQuery(function ($) { } }, getDefaultTemplates = function (urls) { + console.log("def"); var orgID = $('#id_organization').val(), backend = $('#id_config-0-backend').val(); // proceed only if an organization and a backend have been selected @@ -47,7 +49,7 @@ django.jQuery(function ($) { }); }); }, - getRelevantTemplates = function () { + getRelevantTemplates = function (urls) { var orgID = $('#id_organization').val(), backend = $('#id_config-0-backend').val(); if (!orgID || !backend) { @@ -57,30 +59,44 @@ django.jQuery(function ($) { if (orgID.length === 0 || backend.length === 0) { return; } - var url = '/admin/config/device/config/get-relevant-templates/'; + + var url = urls[orgID]; // get relevant templates of selected org and backend - url = url + orgID + '/' + '?backend=' + backend; + url = url + '?backend=' + backend; $.get(url).done(function (data) { var relevantTemplates = {}; $.each(data.templates, function (i, uuid) { relevantTemplates[uuid] = uuid; }); + var disabledTemplates = + localStorage.getItem('ow-disabled-templates', null); + disabledTemplates = JSON.parse(disabledTemplates) || {}; $('input.sortedm2m').each(function () { if (($(this).val() in relevantTemplates) === false) { // hide templates which are not relevant // also removed disabled attribute and uncheck it - $(this).prop("disabled", false); - $(this).prop("checked", false); + if ($(this).prop('disabled')){ + disabledTemplates[$(this).val()] = $(this).val(); + $(this).prop('disabled', false); + $(this).prop('checked', false); + } $(this).parent().parent().hide(); } else { // show templates which are relevant + if ($(this).val() in disabledTemplates){ + $(this).prop('disabled', true); + $(this).prop('checked', true); + delete disabledTemplates[$(this).val()]; + } $(this).parent().parent().show(); } }); + disabledTemplates = JSON.stringify(disabledTemplates) + localStorage.setItem('ow-disabled-templates', disabledTemplates); // change help text if no relevant template - var msg = "Choose items and order by drag & drop."; + var msg = 'Choose items and order by drag & drop.'; if (Object.keys(relevantTemplates).length === 0) { - msg = "No Template available"; + msg = 'No Template available'; } if (gettext) { msg = gettext(msg); @@ -88,29 +104,27 @@ django.jQuery(function ($) { $('.sortedm2m-container > .help').text(msg); }); }, - bindDefaultTemplateLoading = function (urls) { + bindDefaultTemplateLoading = function (default_urls, relevant_urls) { var backendField = $('#id_config-0-backend'); $('#id_organization').change(function () { if ($('#id_config-0-backend').length > 0) { - getRelevantTemplates(); - getDefaultTemplates(urls); + getRelevantTemplates(relevant_urls); + getDefaultTemplates(default_urls); } }); if (backendField.length > 0) { - addChangeEventToBackend(urls); + addChangeEventToBackend(default_urls, relevant_urls); } else { $('#config-group > fieldset.module').ready(function () { $('div.add-row > a').click(function () { - addChangeEventToBackend(urls); - getRelevantTemplates(); - getDefaultTemplates(urls); + addChangeEventToBackend(default_urls, relevant_urls); + getRelevantTemplates(relevant_urls); + getDefaultTemplates(default_urls); }); }); } + getRelevantTemplates(relevant_urls); firstRun = false; }; window.bindDefaultTemplateLoading = bindDefaultTemplateLoading; - $(document).ready(function () { - getRelevantTemplates(); - }); }); diff --git a/openwisp_controller/config/templates/admin/config/change_form.html b/openwisp_controller/config/templates/admin/config/change_form.html index 6c98aa652..8dfeeb202 100644 --- a/openwisp_controller/config/templates/admin/config/change_form.html +++ b/openwisp_controller/config/templates/admin/config/change_form.html @@ -61,7 +61,10 @@ // enable default templates - do not remove this comment (function ($) { $(document).ready( function () { - window.bindDefaultTemplateLoading({{ default_template_urls| safe }}); + window.bindDefaultTemplateLoading( + {{ default_template_urls| safe }}, + {{relevant_template_urls| safe}} + ); }) }) (django.jQuery); From 72f0ac4a1dde2d7835e5e18abbee132d723fc633 Mon Sep 17 00:00:00 2001 From: niteshsinha17 Date: Sat, 13 Mar 2021 11:19:33 +0530 Subject: [PATCH 4/4] [chores] Minor changes --- .../config/static/config/js/default_templates.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openwisp_controller/config/static/config/js/default_templates.js b/openwisp_controller/config/static/config/js/default_templates.js index 8e519303f..2b336146c 100644 --- a/openwisp_controller/config/static/config/js/default_templates.js +++ b/openwisp_controller/config/static/config/js/default_templates.js @@ -2,7 +2,6 @@ django.jQuery(function ($) { var firstRun = true, addChangeEventToBackend = function (default_urls, relevant_urls) { - console.log(default_urls,relevant_urls); $('#id_config-0-backend').change(function () { setTimeout(function () { // ensures getDefaultTemplates execute only after other @@ -21,7 +20,6 @@ django.jQuery(function ($) { } }, getDefaultTemplates = function (urls) { - console.log("def"); var orgID = $('#id_organization').val(), backend = $('#id_config-0-backend').val(); // proceed only if an organization and a backend have been selected @@ -69,13 +67,13 @@ django.jQuery(function ($) { relevantTemplates[uuid] = uuid; }); var disabledTemplates = - localStorage.getItem('ow-disabled-templates', null); + localStorage.getItem('config-disabled-templates', null); disabledTemplates = JSON.parse(disabledTemplates) || {}; $('input.sortedm2m').each(function () { if (($(this).val() in relevantTemplates) === false) { // hide templates which are not relevant // also removed disabled attribute and uncheck it - if ($(this).prop('disabled')){ + if ($(this).prop('disabled')) { disabledTemplates[$(this).val()] = $(this).val(); $(this).prop('disabled', false); $(this).prop('checked', false); @@ -83,7 +81,7 @@ django.jQuery(function ($) { $(this).parent().parent().hide(); } else { // show templates which are relevant - if ($(this).val() in disabledTemplates){ + if ($(this).val() in disabledTemplates) { $(this).prop('disabled', true); $(this).prop('checked', true); delete disabledTemplates[$(this).val()]; @@ -91,8 +89,8 @@ django.jQuery(function ($) { $(this).parent().parent().show(); } }); - disabledTemplates = JSON.stringify(disabledTemplates) - localStorage.setItem('ow-disabled-templates', disabledTemplates); + disabledTemplates = JSON.stringify(disabledTemplates); + localStorage.setItem('config-disabled-templates', disabledTemplates); // change help text if no relevant template var msg = 'Choose items and order by drag & drop.'; if (Object.keys(relevantTemplates).length === 0) {