forked from City-of-Helsinki/parkkihubi
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request City-of-Helsinki#49 from suutari-ai/monitoring-api
Add monitoring API and JWT auth for Dashboard
- Loading branch information
Showing
51 changed files
with
1,299 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import drf_jwt_2fa.urls | ||
from django.conf.urls import include, url | ||
|
||
v1_urlpatterns = [ | ||
url(r'^', include(drf_jwt_2fa.urls, namespace='auth')), | ||
] | ||
|
||
urlpatterns = [ | ||
url(r'^', include(v1_urlpatterns, namespace='v1')), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from django.conf import settings | ||
from rest_framework import permissions | ||
|
||
|
||
class MonitoringApiPermission(permissions.IsAuthenticated): | ||
def has_permission(self, request, view): | ||
if not super().has_permission(request, view): | ||
return False | ||
|
||
user_groups = request.user.groups | ||
group_name = getattr(settings, 'MONITORING_GROUP_NAME', 'monitoring') | ||
|
||
is_in_monitoring_group = (user_groups.filter(name=group_name).exists()) | ||
|
||
return is_in_monitoring_group |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import rest_framework_gis.pagination as gis_pagination | ||
import rest_framework_gis.serializers as gis_serializers | ||
from rest_framework import serializers, viewsets | ||
|
||
from ...models import ParkingArea, Region | ||
from ..common import WGS84InBBoxFilter | ||
from .permissions import MonitoringApiPermission | ||
|
||
WGS84_SRID = 4326 | ||
|
||
# Square meters in square kilometer | ||
M2_PER_KM2 = 1000000.0 | ||
|
||
|
||
class RegionSerializer(gis_serializers.GeoFeatureModelSerializer): | ||
wgs84_geometry = gis_serializers.GeometrySerializerMethodField() | ||
area_km2 = serializers.SerializerMethodField() | ||
spots_per_km2 = serializers.SerializerMethodField() | ||
parking_areas = serializers.SerializerMethodField() | ||
|
||
def get_wgs84_geometry(self, instance): | ||
return instance.geom.transform(WGS84_SRID, clone=True) | ||
|
||
def get_area_km2(self, instance): | ||
return instance.geom.area / M2_PER_KM2 | ||
|
||
def get_spots_per_km2(self, instance): | ||
return M2_PER_KM2 * instance.capacity_estimate / instance.geom.area | ||
|
||
def get_parking_areas(self, instance): | ||
parking_areas = ParkingArea.objects.intersecting_region(instance) | ||
return [x.pk for x in parking_areas] | ||
|
||
class Meta: | ||
model = Region | ||
geo_field = 'wgs84_geometry' | ||
fields = [ | ||
'id', | ||
'name', | ||
'capacity_estimate', | ||
'area_km2', | ||
'spots_per_km2', | ||
'parking_areas', | ||
] | ||
|
||
|
||
class RegionViewSet(viewsets.ReadOnlyModelViewSet): | ||
permission_classes = [MonitoringApiPermission] | ||
queryset = Region.objects.all().order_by('id') | ||
serializer_class = RegionSerializer | ||
pagination_class = gis_pagination.GeoJsonPagination | ||
bbox_filter_field = 'geom' | ||
filter_backends = [WGS84InBBoxFilter] | ||
bbox_filter_include_overlapping = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from rest_framework import serializers, viewsets | ||
|
||
from ...models import Region | ||
from ...pagination import Pagination | ||
from ..common import WGS84InBBoxFilter | ||
from ..utils import parse_timestamp_or_now | ||
from .permissions import MonitoringApiPermission | ||
|
||
|
||
class RegionStatisticsSerializer(serializers.ModelSerializer): | ||
parking_count = serializers.IntegerField(read_only=True) | ||
|
||
class Meta: | ||
model = Region | ||
fields = ( | ||
'id', | ||
'parking_count', | ||
) | ||
|
||
|
||
class RegionStatisticsViewSet(viewsets.ReadOnlyModelViewSet): | ||
permission_classes = [MonitoringApiPermission] | ||
queryset = Region.objects.all() | ||
serializer_class = RegionStatisticsSerializer | ||
pagination_class = Pagination | ||
bbox_filter_field = 'geom' | ||
filter_backends = [WGS84InBBoxFilter] | ||
bbox_filter_include_overlapping = True | ||
|
||
def get_queryset(self): | ||
time = parse_timestamp_or_now(self.request.query_params.get('time')) | ||
return ( | ||
super().get_queryset() | ||
.with_parking_count(time) | ||
.values('id', 'parking_count') | ||
.order_by('id') | ||
.filter(parking_count__gt=0)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from django.conf.urls import include, url | ||
from rest_framework.routers import DefaultRouter | ||
|
||
from .region import RegionViewSet | ||
from .region_statistics import RegionStatisticsViewSet | ||
|
||
router = DefaultRouter() | ||
router.register(r'region', RegionViewSet, base_name='region') | ||
router.register(r'region_statistics', RegionStatisticsViewSet, | ||
base_name='regionstatistics') | ||
|
||
urlpatterns = [ | ||
url(r'^', include(router.urls, namespace='v1')), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import dateutil.parser | ||
from django.utils import timezone | ||
from rest_framework.exceptions import ValidationError | ||
|
||
|
||
def parse_timestamp_or_now(timestamp_string): | ||
""" | ||
Parse given timestamp string or return current time. | ||
If the timestamp string is falsy, return current time, otherwise try | ||
to parse the string and return the parsed value. | ||
:type timestamp_string: str | ||
:rtype: datetime.datetime | ||
:raises rest_framework.exceptions.ValidationError: on parse error | ||
""" | ||
if not timestamp_string: | ||
return timezone.now() | ||
return parse_timestamp(timestamp_string) | ||
|
||
|
||
def parse_timestamp(datetime_string): | ||
try: | ||
return dateutil.parser.parse(datetime_string) | ||
except (ValueError, OverflowError): | ||
raise ValidationError('Invalid timestamp: {}'.format(datetime_string)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,16 @@ | ||
from .operator import OperatorFactory # noqa | ||
from .parking import HistoryParkingFactory, ParkingFactory # noqa | ||
from .parking_area import ParkingAreaFactory # noqa | ||
from .region import RegionFactory | ||
from .user import AdminUserFactory, StaffUserFactory, UserFactory # noqa | ||
|
||
__all__ = [ | ||
'AdminUserFactory', | ||
'HistoryParkingFactory', | ||
'OperatorFactory', | ||
'ParkingAreaFactory', | ||
'ParkingFactory', | ||
'RegionFactory', | ||
'StaffUserFactory', | ||
'UserFactory', | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import factory | ||
|
||
from parkings.models import Region | ||
|
||
from .faker import fake | ||
from .parking_area import generate_multi_polygon | ||
|
||
|
||
class RegionFactory(factory.django.DjangoModelFactory): | ||
class Meta: | ||
model = Region | ||
|
||
geom = factory.LazyFunction(generate_multi_polygon) | ||
capacity_estimate = fake.random.randint(0, 500) | ||
name = factory.LazyFunction(fake.city) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import sys | ||
|
||
from django.contrib.gis.gdal import DataSource | ||
from django.contrib.gis.utils import LayerMapping | ||
|
||
from ..models import Region | ||
|
||
|
||
class ShapeFileToRegionImporter(object): | ||
""" | ||
Importer from ESRI Shapefiles to Region model in the database. | ||
""" | ||
field_mapping = { | ||
# Region model field -> Field in the file | ||
'geom': 'MULTIPOLYGON', | ||
} | ||
|
||
def __init__(self, filename, encoding='utf-8', | ||
output_stream=sys.stderr, verbose=True): | ||
self.filename = filename | ||
self.encoding = encoding | ||
self.data_source = DataSource(filename, encoding=encoding) | ||
self.output_stream = output_stream | ||
self.verbose = verbose | ||
|
||
def set_field_mapping(self, mapping): | ||
self.field_mapping = dict(self.field_mapping, **mapping) | ||
|
||
def get_layer_names(self): | ||
return [layer.name for layer in self.data_source] | ||
|
||
def get_layer_fields(self, name): | ||
layer = self.data_source[self._get_layer_index(name)] | ||
return layer.fields | ||
|
||
def import_from_layer(self, layer_name): | ||
layer_mapping = LayerMapping( | ||
model=Region, | ||
data=self.filename, | ||
mapping=self.field_mapping, | ||
layer=self._get_layer_index(layer_name), | ||
encoding=self.encoding) | ||
silent = (self.output_stream is None) | ||
layer_mapping.save( | ||
strict=True, | ||
stream=self.output_stream, | ||
silent=silent, | ||
verbose=(not silent and self.verbose)) | ||
|
||
def _get_layer_index(self, name): | ||
layer_names = self.get_layer_names() | ||
try: | ||
return layer_names.index(name) | ||
except ValueError: | ||
raise ValueError('No such layer: {!r}'.format(name)) |
Oops, something went wrong.