From 9d4bb24b25fddcc914d6f9fca4689119f8eb6808 Mon Sep 17 00:00:00 2001 From: Johanna England Date: Mon, 6 May 2024 15:27:39 +0200 Subject: [PATCH] Generate ZIP file for download with QR code button --- changelog.d/+qr-code-bulk.added.md | 1 + python/nav/web/seeddb/page/__init__.py | 10 ++- python/nav/web/seeddb/page/netbox/__init__.py | 65 +++++++++++++++++- python/nav/web/seeddb/page/room.py | 49 ++++++++++++- python/nav/web/templates/seeddb/list.html | 4 ++ tests/integration/seeddb_test.py | 68 +++++++++++++++++++ 6 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 changelog.d/+qr-code-bulk.added.md diff --git a/changelog.d/+qr-code-bulk.added.md b/changelog.d/+qr-code-bulk.added.md new file mode 100644 index 0000000000..2efce73dbd --- /dev/null +++ b/changelog.d/+qr-code-bulk.added.md @@ -0,0 +1 @@ +Add button to SeedDB that generates a ZIP file to download with QR Codes linking to the selected netboxes/rooms \ No newline at end of file diff --git a/python/nav/web/seeddb/page/__init__.py b/python/nav/web/seeddb/page/__init__.py index fe236c7853..58c95a1645 100644 --- a/python/nav/web/seeddb/page/__init__.py +++ b/python/nav/web/seeddb/page/__init__.py @@ -42,11 +42,19 @@ def not_implemented(*_args, **_kwargs): raise NotImplementedError() -def view_switcher(request, list_view=None, move_view=None, delete_view=None): +def view_switcher( + request, + list_view=None, + move_view=None, + delete_view=None, + generate_qr_codes_view=None, +): """Selects appropriate view depending on POST data.""" if request.method == 'POST': if 'move' in request.POST: return move_view(request) elif 'delete' in request.POST: return delete_view(request) + elif 'qr_code' in request.POST: + return generate_qr_codes_view(request) return list_view(request) diff --git a/python/nav/web/seeddb/page/netbox/__init__.py b/python/nav/web/seeddb/page/netbox/__init__.py index ad8ce4ab1c..672ae03a1a 100644 --- a/python/nav/web/seeddb/page/netbox/__init__.py +++ b/python/nav/web/seeddb/page/netbox/__init__.py @@ -18,11 +18,14 @@ import datetime from django.db import transaction from django.contrib.postgres.aggregates import ArrayAgg +from django.http import HttpResponseRedirect +from django.urls import reverse from nav.models.manage import Netbox from nav.bulkparse import NetboxBulkParser from nav.bulkimport import NetboxImporter +from nav.web.message import new_message, Messages from nav.web.seeddb import SeeddbInfo, reverse_lazy from nav.web.seeddb.constants import SEEDDB_EDITABLE_MODELS from nav.web.seeddb.page import view_switcher @@ -31,6 +34,10 @@ from nav.web.seeddb.utils.move import move from nav.web.seeddb.utils.bulk import render_bulkimport from nav.web.seeddb.page.netbox.forms import NetboxFilterForm, NetboxMoveForm +from nav.web.utils import ( + generate_qr_codes_as_byte_strings, + generate_qr_codes_as_zip_file, +) class NetboxInfo(SeeddbInfo): @@ -54,7 +61,11 @@ class NetboxInfo(SeeddbInfo): def netbox(request): """Controller for landing page for netboxes""" return view_switcher( - request, list_view=netbox_list, move_view=netbox_move, delete_view=netbox_delete + request, + list_view=netbox_list, + move_view=netbox_move, + delete_view=netbox_delete, + generate_qr_codes_view=netbox_generate_qr_codes, ) @@ -112,6 +123,58 @@ def netbox_pre_deletion_mark(queryset): queryset.update(deleted_at=datetime.datetime.now(), up_to_date=False) +def netbox_generate_qr_codes(request): + """Controller for generating qr codes for netboxes""" + if not request.POST.getlist('object'): + new_message( + request, + "You need to select at least one object to generate qr codes for", + Messages.ERROR, + ) + return HttpResponseRedirect(reverse('seeddb-room')) + + url_dict = dict() + netboxes = Netbox.objects.filter(id__in=request.POST.getlist('object')) + + for netbox in netboxes: + url = request.build_absolute_uri( + reverse('ipdevinfo-details-by-id', kwargs={'netbox_id': netbox.id}) + ) + url_dict[str(netbox)] = url + + qr_codes_zip_file = generate_qr_codes_as_zip_file(url_dict=url_dict) + + info = NetboxInfo() + query = ( + Netbox.objects.select_related("room", "category", "type", "organization") + .prefetch_related("profiles") + .annotate(profile=ArrayAgg("profiles__name")) + ) + filter_form = NetboxFilterForm(request.GET) + value_list = ( + 'sysname', + 'room', + 'ip', + 'category', + 'organization', + 'profile', + 'type__name', + ) + return render_list( + request, + query, + value_list, + 'seeddb-netbox-edit', + edit_url_attr='pk', + filter_form=filter_form, + template='seeddb/list_netbox.html', + extra_context={ + **info.template_context, + **{"qr_codes_zip_file": qr_codes_zip_file}, + }, + ) + + def netbox_move(request): """Controller for handling a move request""" info = NetboxInfo() diff --git a/python/nav/web/seeddb/page/room.py b/python/nav/web/seeddb/page/room.py index a5624a3d48..fe063dba87 100644 --- a/python/nav/web/seeddb/page/room.py +++ b/python/nav/web/seeddb/page/room.py @@ -17,12 +17,14 @@ # """Forms and view functions for SeedDB's Room view""" +from django.http import HttpResponseRedirect from django.urls import reverse from nav.models.manage import Room from nav.bulkparse import RoomBulkParser from nav.bulkimport import RoomImporter +from nav.web.message import new_message, Messages from nav.web.seeddb import SeeddbInfo, reverse_lazy from nav.web.seeddb.constants import SEEDDB_EDITABLE_MODELS from nav.web.seeddb.page import view_switcher @@ -31,6 +33,10 @@ from nav.web.seeddb.utils.delete import render_delete from nav.web.seeddb.utils.move import move from nav.web.seeddb.utils.bulk import render_bulkimport +from nav.web.utils import ( + generate_qr_codes_as_byte_strings, + generate_qr_codes_as_zip_file, +) from ..forms import RoomForm, RoomFilterForm, RoomMoveForm @@ -56,7 +62,11 @@ class RoomInfo(SeeddbInfo): def room(request): """Controller for listing, moving and deleting rooms""" return view_switcher( - request, list_view=room_list, move_view=room_move, delete_view=room_delete + request, + list_view=room_list, + move_view=room_move, + delete_view=room_delete, + generate_qr_codes_view=room_generate_qr_codes, ) @@ -84,6 +94,43 @@ def room_move(request): ) +def room_generate_qr_codes(request): + """Controller for generating qr codes for rooms""" + if not request.POST.getlist('object'): + new_message( + request, + "You need to select at least one object to generate qr codes for", + Messages.ERROR, + ) + return HttpResponseRedirect(reverse('seeddb-room')) + + url_dict = dict() + ids = request.POST.getlist('object') + + for id in ids: + url = request.build_absolute_uri(reverse('room-info', kwargs={'roomid': id})) + url_dict[id] = url + + qr_codes_zip_file = generate_qr_codes_as_zip_file(url_dict=url_dict) + + info = RoomInfo() + value_list = ('id', 'location', 'description', 'position', 'data') + query = Room.objects.select_related("location").all() + filter_form = RoomFilterForm(request.GET) + # When we drop Python 3.7 we can use extra_context = info.template_context | {"qr_codes": qr_codes} + return render_list( + request, + query, + value_list, + 'seeddb-room-edit', + filter_form=filter_form, + extra_context={ + **info.template_context, + **{"qr_codes_zip_file": qr_codes_zip_file}, + }, + ) + + def room_delete(request, object_id=None): """Controller for deleting rooms. Used in room()""" info = RoomInfo() diff --git a/python/nav/web/templates/seeddb/list.html b/python/nav/web/templates/seeddb/list.html index 609dfca722..c90520bd78 100644 --- a/python/nav/web/templates/seeddb/list.html +++ b/python/nav/web/templates/seeddb/list.html @@ -30,6 +30,10 @@ {% endif %} + {% if qr_codes_zip_file %} + Download generated QR Codes + {% endif %} +
diff --git a/tests/integration/seeddb_test.py b/tests/integration/seeddb_test.py index bfd6b6bb97..de3e01d275 100644 --- a/tests/integration/seeddb_test.py +++ b/tests/integration/seeddb_test.py @@ -110,3 +110,71 @@ def test_log_netbox_change_should_not_crash(admin_account, netbox): new.category_id = "OTHER" assert log_netbox_change(admin_account, old, new) is None + + +def test_generating_qr_codes_for_netboxes_should_succeed(client, netbox): + url = reverse('seeddb-netbox') + + response = client.post( + url, + follow=True, + data={ + "qr_code": "Generate+QR+codes+for+selected", + "object": [netbox.id], + }, + ) + + assert response.status_code == 200 + assert 'Download generated QR Codes' in smart_str(response.content) + + +def test_generating_qr_codes_for_no_selected_netboxes_should_show_error(client, netbox): + url = reverse('seeddb-netbox') + + response = client.post( + url, + follow=True, + data={ + "qr_code": "Generate+QR+codes+for+selected", + }, + ) + + assert response.status_code == 200 + assert ( + 'You need to select at least one object to generate qr codes for' + in smart_str(response.content) + ) + + +def test_generating_qr_codes_for_rooms_should_succeed(client): + url = reverse('seeddb-room') + + response = client.post( + url, + follow=True, + data={ + "qr_code": "Generate+QR+codes+for+selected", + "object": ["myroom"], + }, + ) + + assert response.status_code == 200 + assert 'Download generated QR Codes' in smart_str(response.content) + + +def test_generating_qr_codes_for_no_selected_rooms_should_show_error(client, netbox): + url = reverse('seeddb-room') + + response = client.post( + url, + follow=True, + data={ + "qr_code": "Generate+QR+codes+for+selected", + }, + ) + + assert response.status_code == 200 + assert ( + 'You need to select at least one object to generate qr codes for' + in smart_str(response.content) + )