Skip to content

Commit eec111f

Browse files
committed
[fix] Update docs, filter class and tests
1 parent ed1f681 commit eec111f

File tree

3 files changed

+95
-79
lines changed

3 files changed

+95
-79
lines changed

docs/user/rest-api.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -864,9 +864,10 @@ List Indoor Coordinates of a Location
864864

865865
.. note::
866866

867-
this endpoint returns device coordinates from the lowest positive
868-
floor by default. If no positive floor exists, it returns coordinates
869-
from the highest negative floor instead.
867+
this endpoint returns device coordinates from the first floor above
868+
ground (lowest non-negative floors) by default. If a location only has
869+
negative floors (e.g. underground parking lot), then it will return
870+
the closest floor to the ground (maximum negative floor).
870871

871872
.. code-block:: text
872873

openwisp_controller/geo/api/views.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.core.exceptions import ObjectDoesNotExist, ValidationError
22
from django.db.models import Count
33
from django.http import Http404
4+
from django.utils.translation import gettext_lazy as _
45
from django_filters import rest_framework as filters
56
from rest_framework import generics, pagination, status
67
from rest_framework.exceptions import NotFound, PermissionDenied
@@ -52,31 +53,35 @@ class Meta(OrganizationManagedFilter.Meta):
5253
model = FloorPlan
5354

5455

55-
class IndoorCoordinatesFilter(OrganizationManagedFilter):
56-
floor = filters.NumberFilter(field_name="floorplan__floor")
57-
organization = filters.UUIDFilter(field_name="content_object__organization")
56+
class IndoorCoordinatesFilter(filters.FilterSet):
57+
floor = filters.NumberFilter(label=_("Floor"), method="filter_by_floor")
5858

59-
def filter_queryset(self, queryset):
59+
@property
60+
def qs(self):
61+
qs = super().qs
62+
if "floor" not in self.data:
63+
qs = self.filter_by_floor(qs, "floor", None)
64+
return qs
65+
66+
def filter_by_floor(self, queryset, name, value):
6067
"""
6168
If no floor parameter is provided:
62-
Return data for the first available positive floor
63-
If no positive floor exists, return data for the highest negative floor
64-
If a valid floor parameter is provided, return data for that specific floor.
69+
- Return data for the first available non-negative floor.
70+
- If no non-negative floor exists, return data for the maximum negative floor.
6571
"""
66-
organization_managed_qs = OrganizationManagedFilter.filter_queryset(
67-
self, queryset
68-
)
69-
qs = filters.FilterSet.filter_queryset(self, organization_managed_qs)
70-
if "floor" not in self.data:
71-
floors = list(qs.values_list("floorplan__floor", flat=True).distinct())
72-
positives = [f for f in floors if f >= 0]
73-
default_floor = min(positives) if positives else max(floors)
74-
qs = qs.filter(floorplan__floor=default_floor)
75-
return qs
72+
if value is not None:
73+
return queryset.filter(floorplan__floor=value)
74+
# No floor parameter provided
75+
floors = list(queryset.values_list("floorplan__floor", flat=True).distinct())
76+
if not floors:
77+
return queryset.none()
78+
non_negative_floors = [f for f in floors if f >= 0]
79+
default_floor = min(non_negative_floors) if non_negative_floors else max(floors)
80+
return queryset.filter(floorplan__floor=default_floor)
7681

7782
class Meta(OrganizationManagedFilter.Meta):
7883
model = DeviceLocation
79-
fields = OrganizationManagedFilter.Meta.fields + ["floor"]
84+
fields = ["floor"]
8085

8186

8287
class ListViewPagination(pagination.PageNumberPagination):
@@ -279,6 +284,17 @@ def get_queryset(self):
279284
qs = Device.objects.filter(devicelocation__location_id=self.kwargs["pk"])
280285
return qs
281286

287+
def get_has_floorplan(self, qs):
288+
qs = qs.filter(devicelocation__floorplan__isnull=False).exists()
289+
return qs
290+
291+
def list(self, request, *args, **kwargs):
292+
has_floorplan = self.get_has_floorplan(self.get_queryset())
293+
response = super().list(self, request, *args, **kwargs)
294+
if response.status_code == 200:
295+
response.data["has_floorplan"] = has_floorplan
296+
return response
297+
282298

283299
class FloorPlanListCreateView(ProtectedAPIMixin, generics.ListCreateAPIView):
284300
serializer_class = FloorPlanSerializer

openwisp_controller/geo/tests/test_api.py

Lines changed: 57 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
User = get_user_model()
3030

3131

32-
class TestApi(TestGeoMixin, CreateDeviceMixin, TestCase):
32+
class TestApi(TestGeoMixin, TestCase):
3333
url_name = "geo_api:device_coordinates"
3434
object_location_model = DeviceLocation
3535
location_model = Location
@@ -130,7 +130,17 @@ def test_bearer_authentication(self):
130130
username="admin", password="password", is_staff=True, is_superuser=True
131131
)
132132
token = Token.objects.create(user=user).key
133-
device = self._create_object_location().device
133+
device = self._create_object()
134+
location = self._create_location(
135+
organization=device.organization, type="indoor"
136+
)
137+
floor = self._create_floorplan(floor=1, location=location)
138+
self._create_object_location(
139+
content_object=device,
140+
location=location,
141+
floorplan=floor,
142+
organization=device.organization,
143+
)
134144

135145
with self.subTest("Test DeviceLocationView"):
136146
response = self.client.get(
@@ -150,7 +160,6 @@ def test_bearer_authentication(self):
150160
self.assertEqual(response.status_code, 200)
151161

152162
with self.subTest("Test LocationDeviceList"):
153-
location = self._create_location(organization=device.organization)
154163
response = self.client.get(
155164
reverse("geo_api:location_device_list", args=[location.id]),
156165
content_type="application/json",
@@ -159,16 +168,6 @@ def test_bearer_authentication(self):
159168
self.assertEqual(response.status_code, 200)
160169

161170
with self.subTest("Test IndoorCoordinatesList"):
162-
org = self._get_org()
163-
location = self._create_location(organization=org, type="indoor")
164-
floor = self._create_floorplan(floor=1, location=location)
165-
d = self._create_device()
166-
self._create_object_location(
167-
content_object=d,
168-
location=location,
169-
floorplan=floor,
170-
organization=org,
171-
)
172171
response = self.client.get(
173172
reverse("geo_api:indoor_coordinates_list", args=[location.id]),
174173
content_type="application/json",
@@ -335,7 +334,7 @@ def test_indoor_coodinate_list(self):
335334
r = self.client.get(reverse(url, args=[location_b.id]))
336335
self.assertEqual(r.status_code, 404)
337336

338-
with self.subTest("Test indoor coordinate list for org superuser"):
337+
with self.subTest("Test indoor coordinate list for superuser"):
339338
self.client.login(username="admin", password="tester")
340339
r = self.client.get(reverse(url, args=[location_a.id]))
341340
self.assertContains(r, str(device_a.id))
@@ -1110,24 +1109,24 @@ def test_deactivated_device(self):
11101109
def test_indoor_coordinates_list_api(self):
11111110
org = self._create_org(name="Test org")
11121111
location = self._create_location(type="indoor", organization=org)
1113-
f1 = self._create_floorplan(floor=1, location=location)
1114-
f2 = self._create_floorplan(floor=2, location=location)
1115-
d1 = self._create_device(
1112+
floor1 = self._create_floorplan(floor=1, location=location)
1113+
floor2 = self._create_floorplan(floor=2, location=location)
1114+
device1 = self._create_device(
11161115
name="device1", mac_address="00:00:00:00:00:01", organization=org
11171116
)
1118-
d2 = self._create_device(
1117+
device2 = self._create_device(
11191118
name="device2", mac_address="00:00:00:00:00:02", organization=org
11201119
)
11211120
self._create_object_location(
1122-
content_object=d1,
1121+
content_object=device1,
11231122
location=location,
1124-
floorplan=f1,
1123+
floorplan=floor1,
11251124
organization=org,
11261125
)
11271126
self._create_object_location(
1128-
content_object=d2,
1127+
content_object=device2,
11291128
location=location,
1130-
floorplan=f2,
1129+
floorplan=floor2,
11311130
organization=org,
11321131
)
11331132
path = reverse("geo_api:indoor_coordinates_list", args=[location.id])
@@ -1146,34 +1145,34 @@ def test_indoor_coordinates_list_api(self):
11461145

11471146
with self.subTest("Test default floor with all positve floor"):
11481147
location2 = self._create_location(type="indoor", organization=org)
1149-
f0 = self._create_floorplan(floor=0, location=location2)
1150-
f5 = self._create_floorplan(floor=5, location=location2)
1151-
f9 = self._create_floorplan(floor=9, location=location2)
1152-
d0 = self._create_device(
1148+
floor0 = self._create_floorplan(floor=0, location=location2)
1149+
floor5 = self._create_floorplan(floor=5, location=location2)
1150+
floor9 = self._create_floorplan(floor=9, location=location2)
1151+
device0 = self._create_device(
11531152
name="device", mac_address="00:00:00:00:00:00", organization=org
11541153
)
1155-
d5 = self._create_device(
1154+
device5 = self._create_device(
11561155
name="device5", mac_address="00:00:00:00:00:05", organization=org
11571156
)
1158-
d9 = self._create_device(
1157+
device9 = self._create_device(
11591158
name="device9", mac_address="00:00:00:00:00:09", organization=org
11601159
)
11611160
self._create_object_location(
1162-
content_object=d0,
1161+
content_object=device0,
11631162
location=location2,
1164-
floorplan=f0,
1163+
floorplan=floor0,
11651164
organization=org,
11661165
)
11671166
self._create_object_location(
1168-
content_object=d5,
1167+
content_object=device5,
11691168
location=location2,
1170-
floorplan=f5,
1169+
floorplan=floor5,
11711170
organization=org,
11721171
)
11731172
self._create_object_location(
1174-
content_object=d9,
1173+
content_object=device9,
11751174
location=location2,
1176-
floorplan=f9,
1175+
floorplan=floor9,
11771176
organization=org,
11781177
)
11791178
path = reverse("geo_api:indoor_coordinates_list", args=[location2.id])
@@ -1185,34 +1184,34 @@ def test_indoor_coordinates_list_api(self):
11851184

11861185
with self.subTest("Test default floor with all negative floor"):
11871186
location3 = self._create_location(type="indoor", organization=org)
1188-
f_1 = self._create_floorplan(floor=-1, location=location3)
1189-
f_2 = self._create_floorplan(floor=-2, location=location3)
1190-
f_3 = self._create_floorplan(floor=-3, location=location3)
1191-
d_1 = self._create_device(
1187+
floor_1 = self._create_floorplan(floor=-1, location=location3)
1188+
floor_2 = self._create_floorplan(floor=-2, location=location3)
1189+
floor_3 = self._create_floorplan(floor=-3, location=location3)
1190+
device_1 = self._create_device(
11921191
name="device-1", mac_address="00:00:00:00:10:01", organization=org
11931192
)
1194-
d_2 = self._create_device(
1193+
device_2 = self._create_device(
11951194
name="device-2", mac_address="00:00:00:00:10:02", organization=org
11961195
)
1197-
d_3 = self._create_device(
1196+
device_3 = self._create_device(
11981197
name="device-3", mac_address="00:00:00:00:10:03", organization=org
11991198
)
12001199
self._create_object_location(
1201-
content_object=d_1,
1200+
content_object=device_1,
12021201
location=location3,
1203-
floorplan=f_1,
1202+
floorplan=floor_1,
12041203
organization=org,
12051204
)
12061205
self._create_object_location(
1207-
content_object=d_2,
1206+
content_object=device_2,
12081207
location=location3,
1209-
floorplan=f_2,
1208+
floorplan=floor_2,
12101209
organization=org,
12111210
)
12121211
self._create_object_location(
1213-
content_object=d_3,
1212+
content_object=device_3,
12141213
location=location3,
1215-
floorplan=f_3,
1214+
floorplan=floor_3,
12161215
organization=org,
12171216
)
12181217
path = reverse("geo_api:indoor_coordinates_list", args=[location3.id])
@@ -1224,34 +1223,34 @@ def test_indoor_coordinates_list_api(self):
12241223

12251224
with self.subTest("Test default floor with positive and negative floor"):
12261225
location4 = self._create_location(type="indoor", organization=org)
1227-
f_4 = self._create_floorplan(floor=-4, location=location4)
1228-
f0 = self._create_floorplan(floor=0, location=location4)
1229-
f22 = self._create_floorplan(floor=22, location=location4)
1230-
d_3 = self._create_device(
1226+
floor_4 = self._create_floorplan(floor=-4, location=location4)
1227+
floor0 = self._create_floorplan(floor=0, location=location4)
1228+
floor22 = self._create_floorplan(floor=22, location=location4)
1229+
device_3 = self._create_device(
12311230
name="device-4", mac_address="00:00:00:10:10:03", organization=org
12321231
)
1233-
d0 = self._create_device(
1232+
device0 = self._create_device(
12341233
name="device-0", mac_address="00:00:00:00:10:00", organization=org
12351234
)
1236-
d22 = self._create_device(
1235+
device22 = self._create_device(
12371236
name="device22", mac_address="00:00:00:00:10:22", organization=org
12381237
)
12391238
self._create_object_location(
1240-
content_object=d_3,
1239+
content_object=device_3,
12411240
location=location4,
1242-
floorplan=f_4,
1241+
floorplan=floor_4,
12431242
organization=org,
12441243
)
12451244
self._create_object_location(
1246-
content_object=d0,
1245+
content_object=device0,
12471246
location=location4,
1248-
floorplan=f0,
1247+
floorplan=floor0,
12491248
organization=org,
12501249
)
12511250
self._create_object_location(
1252-
content_object=d22,
1251+
content_object=device22,
12531252
location=location4,
1254-
floorplan=f22,
1253+
floorplan=floor22,
12551254
organization=org,
12561255
)
12571256
path = reverse("geo_api:indoor_coordinates_list", args=[location4.id])

0 commit comments

Comments
 (0)