From 98fa484e07e119510f1194673f0dd6961905824a Mon Sep 17 00:00:00 2001 From: Aksiznarf-Uar Date: Mon, 16 Dec 2024 15:35:43 +0100 Subject: [PATCH 1/4] Filtering now refreshes as you type. --- src/nac/subviews/device_management.py | 14 +++++- src/static/device_filtering.js | 30 +++++++++++ src/templates/base.html | 5 +- src/templates/devices.html | 72 +++------------------------ 4 files changed, 55 insertions(+), 66 deletions(-) create mode 100644 src/static/device_filtering.js diff --git a/src/nac/subviews/device_management.py b/src/nac/subviews/device_management.py index 6afad44..94d23e3 100644 --- a/src/nac/subviews/device_management.py +++ b/src/nac/subviews/device_management.py @@ -4,6 +4,8 @@ from django.urls import reverse_lazy from django.shortcuts import render import json +from django.http import JsonResponse +from django.template.loader import render_to_string from ..models import Device, AuthorizationGroup, DeviceRoleProd from ..forms import DeviceForm, DeviceSearchForm @@ -38,8 +40,18 @@ def get_queryset(self): device_list = device_list.filter(appl_NAC_DeviceRoleProd__in=selected_device_roles_prod) return device_list.order_by("name") + def get(self, request, *args, **kwargs): + # Check if the request is an AJAX request + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + # Handle AJAX request by rendering only the relevant part of the template + html = render_to_string('devices_results.html', {"device_list": self.get_queryset()}) + return JsonResponse({'html': html}) + + # Otherwise, handle a normal HTTP request + return super().get(request, *args, **kwargs) + # we need this for the drop-down menus with filtering options - def get_context_data(self, *, object_list=None, **kwargs): + def get_context_data(self, **kwargs): context = super(DeviceListView, self).get_context_data(**kwargs) context["auth_group_list"] = AuthorizationGroup.objects.filter(id__in=self.request.user.authorization_group.all()) context["device_role_prod_list"] = DeviceRoleProd.objects.all() diff --git a/src/static/device_filtering.js b/src/static/device_filtering.js new file mode 100644 index 0000000..8dec349 --- /dev/null +++ b/src/static/device_filtering.js @@ -0,0 +1,30 @@ +const search_form = $("#search_form") +const results_div = $('#results') +const endpoint = '/devices/' +const delay_by_in_ms = 700 +let scheduled_function = false + +let ajax_call = function (endpoint, request_parameters) { + $.getJSON(endpoint, request_parameters) + .done(response => { + // replace the HTML contents + results_div.html(response['html']) + }) +} + +search_form.on('input', function () { + + const request_parameters = { + search_string: $('#id_search_string').val(), + device_role_prod: $('#id_device_role_prod').val(), + authorization_group: $('#id_authorization_group').val() + } + + // if scheduled_function is NOT false, cancel the execution of the function + if (scheduled_function) { + clearTimeout(scheduled_function) + } + + // setTimeout returns the ID of the function to be executed + scheduled_function = setTimeout(ajax_call, delay_by_in_ms, endpoint, request_parameters); +}) \ No newline at end of file diff --git a/src/templates/base.html b/src/templates/base.html index 8e98d94..052952b 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -15,7 +15,10 @@ - + + + + {% block head %}{% endblock %} diff --git a/src/templates/devices.html b/src/templates/devices.html index c764758..66ce164 100644 --- a/src/templates/devices.html +++ b/src/templates/devices.html @@ -5,7 +5,7 @@ {% block content %}

Devices

-
+
{% for field in search_form %}
@@ -15,68 +15,12 @@

Devices

- - - - - - - - - - - - {% for device in device_list %} - - - - - - - - - {% endfor %} -
NameStatusFQDNDevice Role ProdMAC
- {{ device.name }} - - {% if device.appl_NAC_Active %} - appl_NAC_Active_True - {% else %} - appl_NAC_Active_False - {% endif %} - {% if device.appl_NAC_Install %} - appl_NAC_Install - {% endif %} - - {{ device.appl_NAC_FQDN }} - - {{ device.appl_NAC_DeviceRoleProd }} - - {% for mac in device.get_appl_NAC_macAddressAIR %} - - {% if mac != None %} - appl_NAC_macAddressAIR - {{mac}}
- {% endif %} -
- {% endfor %} - {% for mac in device.get_appl_NAC_macAddressCAB %} - - {% if mac != None %} - appl_NAC_macAddressCAB - {{mac}}
- {% endif %} -
- {% endfor %} -
- - edit - - - - delete - -
+
+ {% include "devices_results.html" %} +
-{% endblock content %} + + + +{% endblock content %} \ No newline at end of file From 93c78b83e39e253750cabf08e01c9a47b90daa2d Mon Sep 17 00:00:00 2001 From: Aksiznarf-Uar Date: Thu, 19 Dec 2024 15:50:41 +0100 Subject: [PATCH 2/4] Added devices_results.html --- src/templates/devices_results.html | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/templates/devices_results.html diff --git a/src/templates/devices_results.html b/src/templates/devices_results.html new file mode 100644 index 0000000..de390e6 --- /dev/null +++ b/src/templates/devices_results.html @@ -0,0 +1,66 @@ +{% load static %} + + + + + + + + + + + + + + {% for device in device_list %} + + + + + + + + + {% endfor %} +
NameStatusFQDNDevice Role ProdMAC
+ {{ device.name }} + + {% if device.appl_NAC_Active %} + appl_NAC_Active_True + {% else %} + appl_NAC_Active_False + {% endif %} + {% if device.appl_NAC_Install %} + appl_NAC_Install + {% endif %} + + {{ device.appl_NAC_FQDN }} + + {{ device.appl_NAC_DeviceRoleProd }} + + {% for mac in device.get_appl_NAC_macAddressAIR %} + + {% if mac != None %} + appl_NAC_macAddressAIR + {{mac}}
+ {% endif %} +
+ {% endfor %} + {% for mac in device.get_appl_NAC_macAddressCAB %} + + {% if mac != None %} + appl_NAC_macAddressCAB + {{mac}}
+ {% endif %} +
+ {% endfor %} +
+ + edit + + + + delete + +
+
\ No newline at end of file From fb2aec0100d27dee6aefc0dd3b98da12d6cb6098 Mon Sep 17 00:00:00 2001 From: Aksiznarf-Uar Date: Thu, 9 Jan 2025 17:32:19 +0100 Subject: [PATCH 3/4] Added test --- src/tests/test_views/test_device_page.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/tests/test_views/test_device_page.py b/src/tests/test_views/test_device_page.py index 48df91e..bb3d804 100644 --- a/src/tests/test_views/test_device_page.py +++ b/src/tests/test_views/test_device_page.py @@ -33,6 +33,28 @@ def test_device_search(query, result): assertQuerySetEqual(desired_qs, result_qs, ordered=False) +@pytest.mark.django_db +def test_result_rendering(client): + test_user = CustomUser.objects.create(name="test") + test_user.set_password("test") + test_user.authorization_group.set([AuthorizationGroup.objects.get(pk=1)]) + test_user.save() + + client.force_login(test_user) + + url = reverse_lazy('devices') + response = client.get(url) + assert response.status_code == 200 + + ajax_response = client.get( + url, # The URL where the AJAX request is sent + {"search_string": "", "authorization_group": "", "device_role_prod": ""}, # Parameters to be sent in the AJAX request + HTTP_X_REQUESTED_WITH='XMLHttpRequest' # Indicate it's an AJAX request + ) + + assert ajax_response.status_code == 200 + + @pytest.mark.django_db @pytest.mark.parametrize("auth_group, device_role_prod, result", [("", "", [1, 2, 3, 4, 5]), From 5295a7423af6a50eaa95a1c4f5ba88bac5594cc9 Mon Sep 17 00:00:00 2001 From: Aksiznarf-Uar Date: Fri, 10 Jan 2025 16:28:15 +0100 Subject: [PATCH 4/4] Added LoginRequiredMixin to views --- src/nac/subviews/account.py | 5 ++++- src/nac/subviews/armis.py | 4 +++- src/nac/subviews/autocomplete.py | 7 ++++--- src/nac/subviews/device_management.py | 11 ++++++----- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/nac/subviews/account.py b/src/nac/subviews/account.py index 9326f25..a78a7b1 100644 --- a/src/nac/subviews/account.py +++ b/src/nac/subviews/account.py @@ -1,14 +1,17 @@ from django.contrib import messages from django.contrib.auth import update_session_auth_hash +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordChangeForm from django.shortcuts import render, redirect from django.views.generic import TemplateView -class AccountSettings(TemplateView): +class AccountSettings(LoginRequiredMixin, TemplateView): template_name = "account_settings.html" +@login_required def change_password(request): if request.method == 'POST': form = PasswordChangeForm(request.user, request.POST) diff --git a/src/nac/subviews/armis.py b/src/nac/subviews/armis.py index a3ff8a5..10e7b4f 100644 --- a/src/nac/subviews/armis.py +++ b/src/nac/subviews/armis.py @@ -16,11 +16,13 @@ from django.views.generic import View from django.core.cache import cache from django.shortcuts import render +from django.contrib.auth.mixins import LoginRequiredMixin + from helper.armis import get_armis_sites, get_devices, get_tenant_url, get_boundaries, map_ids_to_names -class ArmisView(View): +class ArmisView(LoginRequiredMixin, View): template_name = "armis_import.html" def _get_context(self): # sets the site-context for armis_import.html, uses cache to be less time consuming diff --git a/src/nac/subviews/autocomplete.py b/src/nac/subviews/autocomplete.py index 03be550..2c0480e 100644 --- a/src/nac/subviews/autocomplete.py +++ b/src/nac/subviews/autocomplete.py @@ -1,8 +1,9 @@ from dal import autocomplete from ..models import DeviceRoleProd, AuthorizationGroup, DeviceRoleInst +from django.contrib.auth.mixins import LoginRequiredMixin -class DeviceRoleProdAutocomplete(autocomplete.Select2QuerySetView): +class DeviceRoleProdAutocomplete(LoginRequiredMixin, autocomplete.Select2QuerySetView): def get_queryset(self): if not self.request.user.is_authenticated: return DeviceRoleProd.objects.none() @@ -23,7 +24,7 @@ def get_queryset(self): return qs -class DeviceRoleInstAutocomplete(autocomplete.Select2QuerySetView): +class DeviceRoleInstAutocomplete(LoginRequiredMixin, autocomplete.Select2QuerySetView): def get_queryset(self): if not self.request.user.is_authenticated: return DeviceRoleInst.objects.none() @@ -40,7 +41,7 @@ def get_queryset(self): return qs -class AuthorizationGroupAutocomplete(autocomplete.Select2QuerySetView): +class AuthorizationGroupAutocomplete(LoginRequiredMixin, autocomplete.Select2QuerySetView): def get_queryset(self): if not self.request.user.is_authenticated: return AuthorizationGroup.objects.none() diff --git a/src/nac/subviews/device_management.py b/src/nac/subviews/device_management.py index 6afad44..293f7c5 100644 --- a/src/nac/subviews/device_management.py +++ b/src/nac/subviews/device_management.py @@ -3,6 +3,7 @@ from django.db.models import Q from django.urls import reverse_lazy from django.shortcuts import render +from django.contrib.auth.mixins import LoginRequiredMixin import json from ..models import Device, AuthorizationGroup, DeviceRoleProd @@ -10,7 +11,7 @@ from ..validation import normalize_mac -class DeviceListView(ListView): +class DeviceListView(LoginRequiredMixin, ListView): model = Device template_name = "devices.html" context_object_name = "device_list" @@ -47,24 +48,24 @@ def get_context_data(self, *, object_list=None, **kwargs): return context -class DeviceDetailView(DetailView): +class DeviceDetailView(LoginRequiredMixin, DetailView): model = Device template_name = "device_detail.html" -class DeviceUpdateView(UpdateView): +class DeviceUpdateView(LoginRequiredMixin, UpdateView): model = Device form_class = DeviceForm template_name = "device_edit.html" -class DeviceDeleteView(DeleteView): +class DeviceDeleteView(LoginRequiredMixin, DeleteView): model = Device template_name = "device_delete.html" success_url = reverse_lazy("devices") -class DeviceCreateView(CreateView): +class DeviceCreateView(LoginRequiredMixin, CreateView): model = Device form_class = DeviceForm template_name = "device_new.html"