11from django .core .exceptions import ObjectDoesNotExist , ValidationError
22from django .db .models import Count
33from django .http import Http404
4+ from django .utils .translation import gettext_lazy as _
45from django_filters import rest_framework as filters
56from rest_framework import generics , pagination , status
67from rest_framework .exceptions import NotFound , PermissionDenied
1415from openwisp_users .api .filters import OrganizationManagedFilter
1516from openwisp_users .api .mixins import FilterByOrganizationManaged , FilterByParentManaged
1617
17- from ...mixins import ProtectedAPIMixin , RelatedDeviceProtectedAPIMixin
18+ from ...mixins import (
19+ BaseProtectedAPIMixin ,
20+ ProtectedAPIMixin ,
21+ RelatedDeviceProtectedAPIMixin ,
22+ )
1823from .filters import DeviceListFilter
1924from .serializers import (
2025 DeviceCoordinatesSerializer ,
2126 DeviceLocationSerializer ,
2227 FloorPlanSerializer ,
2328 GeoJsonLocationSerializer ,
29+ IndoorCoordinatesSerializer ,
2430 LocationDeviceSerializer ,
2531 LocationSerializer ,
2632)
@@ -51,6 +57,37 @@ class Meta(OrganizationManagedFilter.Meta):
5157 model = FloorPlan
5258
5359
60+ class IndoorCoordinatesFilter (filters .FilterSet ):
61+ floor = filters .NumberFilter (label = _ ("Floor" ), method = "filter_by_floor" )
62+
63+ @property
64+ def qs (self ):
65+ qs = super ().qs
66+ if "floor" not in self .data :
67+ qs = self .filter_by_floor (qs , "floor" , None )
68+ return qs
69+
70+ def filter_by_floor (self , queryset , name , value ):
71+ """
72+ If no floor parameter is provided:
73+ - Return data for the first available non-negative floor.
74+ - If no non-negative floor exists, return data for the maximum negative floor.
75+ """
76+ if value is not None :
77+ return queryset .filter (floorplan__floor = value )
78+ # No floor parameter provided
79+ floors = list (queryset .values_list ("floorplan__floor" , flat = True ).distinct ())
80+ if not floors :
81+ return queryset .none ()
82+ non_negative_floors = [f for f in floors if f >= 0 ]
83+ default_floor = min (non_negative_floors ) if non_negative_floors else max (floors )
84+ return queryset .filter (floorplan__floor = default_floor )
85+
86+ class Meta (OrganizationManagedFilter .Meta ):
87+ model = DeviceLocation
88+ fields = ["floor" ]
89+
90+
5491class ListViewPagination (pagination .PageNumberPagination ):
5592 page_size = 10
5693 page_size_query_param = "page_size"
@@ -187,6 +224,47 @@ class GeoJsonLocationList(
187224 filterset_class = LocationOrganizationFilter
188225
189226
227+ class IndoorCoordinatesViewPagination (ListViewPagination ):
228+ page_size = 50
229+
230+
231+ class IndoorCoordinatesList (
232+ FilterByParentManaged , BaseProtectedAPIMixin , generics .ListAPIView
233+ ):
234+ serializer_class = IndoorCoordinatesSerializer
235+ filter_backends = [filters .DjangoFilterBackend ]
236+ filterset_class = IndoorCoordinatesFilter
237+ pagination_class = IndoorCoordinatesViewPagination
238+ queryset = (
239+ DeviceLocation .objects .filter (
240+ location__type = "indoor" ,
241+ floorplan__isnull = False ,
242+ )
243+ .select_related (
244+ "content_object" , "location" , "floorplan" , "location__organization"
245+ )
246+ .order_by ("floorplan__floor" )
247+ )
248+
249+ def get_parent_queryset (self ):
250+ qs = Location .objects .filter (pk = self .kwargs ["pk" ])
251+ return qs
252+
253+ def get_queryset (self ):
254+ return super ().get_queryset ().filter (location_id = self .kwargs ["pk" ])
255+
256+ def get_available_floors (self , qs ):
257+ floors = list (qs .values_list ("floorplan__floor" , flat = True ).distinct ())
258+ return floors
259+
260+ def list (self , request , * args , ** kwargs ):
261+ floors = self .get_available_floors (self .get_queryset ())
262+ response = super ().list (request , * args , ** kwargs )
263+ if response .status_code == 200 :
264+ response .data ["floors" ] = floors
265+ return response
266+
267+
190268class LocationDeviceList (
191269 FilterByParentManaged , ProtectedAPIMixin , generics .ListAPIView
192270):
@@ -203,6 +281,17 @@ def get_queryset(self):
203281 qs = Device .objects .filter (devicelocation__location_id = self .kwargs ["pk" ])
204282 return qs
205283
284+ def get_has_floorplan (self , qs ):
285+ qs = qs .filter (devicelocation__floorplan__isnull = False ).exists ()
286+ return qs
287+
288+ def list (self , request , * args , ** kwargs ):
289+ has_floorplan = self .get_has_floorplan (self .get_queryset ())
290+ response = super ().list (request , * args , ** kwargs )
291+ if response .status_code == 200 :
292+ response .data ["has_floorplan" ] = has_floorplan
293+ return response
294+
206295
207296class FloorPlanListCreateView (ProtectedAPIMixin , generics .ListCreateAPIView ):
208297 serializer_class = FloorPlanSerializer
@@ -245,5 +334,6 @@ class LocationDetailView(
245334location_device_list = LocationDeviceList .as_view ()
246335list_floorplan = FloorPlanListCreateView .as_view ()
247336detail_floorplan = FloorPlanDetailView .as_view ()
337+ indoor_coordinates_list = IndoorCoordinatesList .as_view ()
248338list_location = LocationListCreateView .as_view ()
249339detail_location = LocationDetailView .as_view ()
0 commit comments