Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Add geojson endpoints #360 #374

Merged
merged 11 commits into from
Feb 18, 2021
58 changes: 57 additions & 1 deletion openwisp_controller/geo/api/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from django.urls import reverse
from rest_framework import generics
from rest_framework.permissions import BasePermission
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.serializers import IntegerField, SerializerMethodField
from rest_framework_gis import serializers as gis_serializers
from rest_framework_gis.pagination import GeoJsonPagination
from swapper import load_model

from openwisp_users.api.mixins import FilterByOrganizationManaged, FilterByParentManaged
from openwisp_utils.api.serializers import ValidatedModelSerializer

Device = load_model('config', 'Device')
Location = load_model('geo', 'Location')
DeviceLocation = load_model('geo', 'DeviceLocation')
Expand All @@ -22,6 +29,28 @@ class Meta:
read_only_fields = ('name',)


class DeviceSerializer(ValidatedModelSerializer):
admin_edit_url = SerializerMethodField('get_admin_edit_url')

def get_admin_edit_url(self, obj):
return self.context['request'].build_absolute_uri(
reverse(f'admin:{obj._meta.app_label}_device_change', args=(obj.id,))
)

class Meta:
model = Device
fields = '__all__'


class GeoJsonLocationSerializer(gis_serializers.GeoFeatureModelSerializer):
device_count = IntegerField()

class Meta:
model = Location
geo_field = 'geometry'
fields = '__all__'


class DeviceLocationView(generics.RetrieveUpdateAPIView):
serializer_class = LocationSerializer
permission_classes = (DevicePermission,)
Expand Down Expand Up @@ -58,4 +87,31 @@ def create_location(self, device):
return location


class GeoJsonLocationList(FilterByOrganizationManaged, generics.ListAPIView):
GeoJsonPagination.page_size = 1000
permission_classes = (IsAuthenticated,)
pagination_class = GeoJsonPagination
queryset = Location.objects.filter(devicelocation__isnull=False).annotate(
device_count=Count('devicelocation')
)
serializer_class = GeoJsonLocationSerializer


class LocationDeviceList(FilterByParentManaged, generics.ListAPIView):
serializer_class = DeviceSerializer
permission_classes = (IsAuthenticated,)
queryset = Device.objects.none()
purhan marked this conversation as resolved.
Show resolved Hide resolved

def get_parent_queryset(self):
qs = Location.objects.filter(pk=self.kwargs['pk'])
return qs

def get_queryset(self):
super().get_queryset()
qs = Device.objects.filter(devicelocation__location_id=self.kwargs['pk'])
return qs


device_location = DeviceLocationView.as_view()
geojson = GeoJsonLocationList.as_view()
location_device_list = LocationDeviceList.as_view()
93 changes: 93 additions & 0 deletions openwisp_controller/geo/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
from django.urls import reverse
from swapper import load_model

from openwisp_controller.config.tests.utils import CreateConfigTemplateMixin
from openwisp_users.tests.utils import TestOrganizationMixin
from openwisp_utils.tests import capture_any_output

from .utils import TestGeoMixin

Device = load_model('config', 'Device')
Location = load_model('geo', 'Location')
DeviceLocation = load_model('geo', 'DeviceLocation')
OrganizationUser = load_model('openwisp_users', 'OrganizationUser')


class TestApi(TestGeoMixin, TestCase):
Expand Down Expand Up @@ -83,3 +88,91 @@ def test_put_update_coordinates(self):
},
)
self.assertEqual(self.location_model.objects.count(), 1)


class TestMultitenantApi(
TestOrganizationMixin, TestGeoMixin, TestCase, CreateConfigTemplateMixin
):
object_location_model = DeviceLocation
location_model = Location
object_model = Device

def setUp(self):
super().setUp()
# create 2 orgs
self._create_org(name='org_b', slug='org_b')
org_a = self._create_org(name='org_a', slug='org_a')
# create an operator for org_a
ou = OrganizationUser.objects.create(
user=self._create_operator(), organization=org_a
)
ou.is_admin = True
ou.save()
# create a superuser
self._create_admin(is_superuser=True,)

def _create_device_location(self, **kwargs):
options = dict()
options.update(kwargs)
device_location = self.object_location_model(**options)
device_location.full_clean()
device_location.save()
return device_location

@capture_any_output()
def test_location_device_list(self):
url = 'geo:api_location_device_list'
# create 2 devices and 2 device location for each org
device_a = self._create_device(organization=self._get_org('org_a'))
device_b = self._create_device(organization=self._get_org('org_b'))
location_a = self._create_location(organization=self._get_org('org_a'))
location_b = self._create_location(organization=self._get_org('org_b'))
self._create_device_location(content_object=device_a, location=location_a)
self._create_device_location(content_object=device_b, location=location_b)

with self.subTest('Test location device list for org operator'):
self.client.login(username='operator', password='tester')
r = self.client.get(reverse(url, args=[location_a.id]))
self.assertContains(r, str(device_a.id))
r = self.client.get(reverse(url, args=[location_b.id]))
self.assertEqual(r.status_code, 404)

with self.subTest('Test location device list for org superuser'):
self.client.login(username='admin', password='tester')
r = self.client.get(reverse(url, args=[location_a.id]))
self.assertContains(r, str(device_a.id))
r = self.client.get(reverse(url, args=[location_b.id]))
self.assertContains(r, str(device_b.id))

with self.subTest('Test location device list for unauthenticated user'):
self.client.logout()
r = self.client.get(reverse(url, args=[location_a.id]))
self.assertEqual(r.status_code, 403)

@capture_any_output()
def test_geojson_list(self):
url = 'geo:api_geojson'
# create 2 devices and 2 device location for each org
device_a = self._create_device(organization=self._get_org('org_a'))
device_b = self._create_device(organization=self._get_org('org_b'))
location_a = self._create_location(organization=self._get_org('org_a'))
location_b = self._create_location(organization=self._get_org('org_b'))
self._create_device_location(content_object=device_a, location=location_a)
self._create_device_location(content_object=device_b, location=location_b)

with self.subTest('Test geojson list for org operator'):
self.client.login(username='operator', password='tester')
r = self.client.get(reverse(url))
self.assertContains(r, str(location_a.pk))
self.assertNotContains(r, str(location_b.pk))

with self.subTest('Test geojson list for superuser'):
self.client.login(username='admin', password='tester')
r = self.client.get(reverse(url))
self.assertContains(r, str(location_a.pk))
self.assertContains(r, str(location_b.pk))

with self.subTest('Test geojson list unauthenticated user'):
self.client.logout()
r = self.client.get(reverse(url))
self.assertEqual(r.status_code, 403)
6 changes: 6 additions & 0 deletions openwisp_controller/geo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ def get_geo_urls(geo_views):
geo_views.device_location,
name='api_device_location',
),
url(r'^api/v1/device/geojson/$', geo_views.geojson, name='api_geojson',),
url(
r'^api/v1/location/(?P<pk>[^/]+)/device/$',
geo_views.location_device_list,
name='api_location_device_list',
),
]
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ django-x509~=0.9.2
django-taggit~=1.3.0
django-loci~=0.4.0
django-flat-json-widget~=0.1.2
openwisp-users~=0.5.1
# TODO: change this when next version of openwisp_users is released
openwisp-users @ https://github.com/purhan/openwisp-users/tarball/master
purhan marked this conversation as resolved.
Show resolved Hide resolved
openwisp-utils[rest]~=0.7.1
openwisp-notifications~=0.3
djangorestframework-gis>=0.12.0,<0.17.0
Expand Down
16 changes: 16 additions & 0 deletions tests/openwisp2/sample_geo/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
from openwisp_controller.geo.api.views import (
DeviceLocationView as BaseDeviceLocationView,
)
from openwisp_controller.geo.api.views import (
GeoJsonLocationList as BaseGeoJsonLocationList,
)
from openwisp_controller.geo.api.views import (
LocationDeviceList as BaseLocationDeviceList,
)


class DeviceLocationView(BaseDeviceLocationView):
pass


class GeoJsonLocationList(BaseGeoJsonLocationList):
pass


class LocationDeviceList(BaseLocationDeviceList):
pass


device_location = DeviceLocationView.as_view()
geojson = GeoJsonLocationList.as_view()
location_device_list = LocationDeviceList.as_view()