From ad399ccd22379874260da28dc4c5eb7cbac920a6 Mon Sep 17 00:00:00 2001 From: Srinath0916 Date: Fri, 19 Dec 2025 23:37:52 +0530 Subject: [PATCH 1/2] [fix] Fixed MultiValueDictKeyError on empty device form submission #1057 Fixes #1057 --------- Co-authored-by: Gagan Deep Co-authored-by: Federico Capoano (cherry picked from commit 828dfb30dcc1378e7dc592a662f4d0aeeeb1ce04) --- openwisp_controller/config/admin.py | 15 ++++++++-- .../config/tests/test_admin.py | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/openwisp_controller/config/admin.py b/openwisp_controller/config/admin.py index b78d048f6..9c758fd06 100644 --- a/openwisp_controller/config/admin.py +++ b/openwisp_controller/config/admin.py @@ -352,10 +352,13 @@ def get_temp_model_instance(self, **options): config_model = self.Meta.model instance = config_model(**options) device_model = config_model.device.field.related_model - org = Organization.objects.get(pk=self.data["organization"]) + if not (org_id := self.data.get("organization")): + # We cannot validate the templates without an organization. + return + org = Organization.objects.get(pk=org_id) instance.device = device_model( - name=self.data["name"], - mac_address=self.data["mac_address"], + name=self.data.get("name", ""), + mac_address=self.data.get("mac_address", ""), organization=org, ) return instance @@ -369,6 +372,12 @@ def clean_templates(self): # when adding self.instance is empty, we need to create a # temporary instance that we'll use just for validation config = self.get_temp_model_instance(**data) + if not config: + # The request does not contain vaild data to create a temporary + # Device instance. Thus, we cannot validate the templates. + # The Device validation will be handled by DeviceAdmin. + # Therefore, we don't need to raise any error here. + return else: config = self.instance if config.backend and templates: diff --git a/openwisp_controller/config/tests/test_admin.py b/openwisp_controller/config/tests/test_admin.py index c33b402c4..3be6f448a 100644 --- a/openwisp_controller/config/tests/test_admin.py +++ b/openwisp_controller/config/tests/test_admin.py @@ -2279,6 +2279,36 @@ def test_templates_fetch_queries_10(self): config = self._create_config(organization=self._get_org()) self._verify_template_queries(config, 10) + def test_empty_device_form_with_config_inline(self): + org = self._get_org() + template = self._create_template(organization=org) + path = reverse(f"admin:{self.app_label}_device_add") + # Submit form without required device fields but with config inline + # This reproduces the scenario where user clicks "Add another Configuration" + # and submits without filling device details + params = { + "config-0-backend": "netjsonconfig.OpenWrt", + "config-0-templates": str(template.pk), + "config-0-config": json.dumps({}), + "config-0-context": "", + "config-TOTAL_FORMS": 1, + "config-INITIAL_FORMS": 0, + "config-MIN_NUM_FORMS": 0, + "config-MAX_NUM_FORMS": 1, + "deviceconnection_set-TOTAL_FORMS": 0, + "deviceconnection_set-INITIAL_FORMS": 0, + "deviceconnection_set-MIN_NUM_FORMS": 0, + "deviceconnection_set-MAX_NUM_FORMS": 1000, + "command_set-TOTAL_FORMS": 0, + "command_set-INITIAL_FORMS": 0, + "command_set-MIN_NUM_FORMS": 0, + "command_set-MAX_NUM_FORMS": 1000, + } + response = self.client.post(path, params) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "errorlist") + self.assertEqual(Device.objects.count(), 0) + class TestTransactionAdmin( CreateConfigTemplateMixin, From ce13840bdc9dbfafd6962785d9688bc6c32027b9 Mon Sep 17 00:00:00 2001 From: Sarthak Tyagi <142912014+stktyagi@users.noreply.github.com> Date: Fri, 16 Jan 2026 03:22:45 +0530 Subject: [PATCH 2/2] [fix] Fixed 500 FieldError in DeviceLocationView #1110 Fixes #1110 [backport 1.2] (cherry picked from commit 6697a3a8820c2bd10960edfb9ceec52fea6a8488) --- openwisp_controller/geo/api/views.py | 1 + openwisp_controller/geo/tests/test_api.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/openwisp_controller/geo/api/views.py b/openwisp_controller/geo/api/views.py index b5514cf26..eb923abe4 100644 --- a/openwisp_controller/geo/api/views.py +++ b/openwisp_controller/geo/api/views.py @@ -115,6 +115,7 @@ class DeviceLocationView( lookup_field = "content_object" lookup_url_kwarg = "pk" organization_field = "content_object__organization" + organization_lookup = "organization__in" _device_field = "content_object" def get_queryset(self): diff --git a/openwisp_controller/geo/tests/test_api.py b/openwisp_controller/geo/tests/test_api.py index 4cc37519f..8c103784b 100644 --- a/openwisp_controller/geo/tests/test_api.py +++ b/openwisp_controller/geo/tests/test_api.py @@ -9,6 +9,7 @@ from django.urls import reverse from django.urls.exceptions import NoReverseMatch from PIL import Image +from rest_framework import status from rest_framework.authtoken.models import Token from swapper import load_model @@ -1036,3 +1037,20 @@ def test_deactivated_device(self): with self.subTest("Test deleting DeviceLocation"): response = self.client.delete(url) self.assertEqual(response.status_code, 403) + + def test_device_location_view_parent_permission(self): + org1 = self._create_org(name="Org One") + device1 = self._create_device(organization=org1) + org2 = self._create_org(name="Org Two") + manager_org2 = self._create_administrator( + organizations=[org2], + username="manager_org2", + password="test_password", + is_superuser=False, + is_staff=True, + ) + self.client.force_login(manager_org2) + url = reverse("geo_api:device_location", args=[device1.pk]) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.client.logout()