Skip to content

Commit 1e14cb7

Browse files
esirKKhadijaMahangakilemensi
authored
Update main branch with develop branch changes (#98)
* Ft locations endpoint (#75) * Adds Location Serializer * Allows users to list and create locations * Adds a basename for sensors_location_router * Adds Pagination * Adds Node creation API endpoint (#76) * Adds Node creation API endpoint * Remove un used serializer import * Add Sensors + Sensor_Type Endpoint (#77) * sensors endpoint * fix serializers * authentication on post * sensors type endpoint * Update sensorsafrica/api/v2/views.py Co-authored-by: _ Kilemensi <kilemensi@users.noreply.github.com> * Update sensorsafrica/api/v2/views.py Co-authored-by: _ Kilemensi <kilemensi@users.noreply.github.com> * spelling Co-authored-by: _ Kilemensi <kilemensi@users.noreply.github.com> * [Hotfix] Creation api (#79) * Standardize naming around views, models and routes * Add tests * [Feature] Public / Private Data (#81) * Add SensorDataView using CursorPagination to show all available data * Make /v2/data return all data, keept /data/<sensortype> for air/water, etc. * Switch computation to only work with public data (sensorsAFRICA data for now) * Upgrade backward compatible deps * Add default AUTHENTICATION_CLASSES (#82) Feinstaub framework doesn't set per view authentication classes and hence we need default ones * [Fix] serializer (#83) * Introduce SensorTypeSerializer that will handle uid * Use SensorTypeSerializer * Ensure city and data exists before trying to upload (#85) * Upload data from public sensors only (#87) * [Feature] Make v1/now return public data only (#86) * Recreate feinstaub NowView but with public sensors filter * Switch to custom NowView * Add owner as part of node (#89) * [Fix] v1/node should check groups (#88) * Add NodeView that checks group ownership + pagination * Update router to use new NodeView * Use user id instead of user object (#90) * Adds endpoint for providing metadata (#91) * Adds endpoint for providing metadata * Use dynamic database name * Returns sensors locations and last time database was updated. * Adds sensor networks to returned metadata * Adds authentication to the meta endpoint * Remove a default NETWORKS_OWNER * Use latest created SensorDataValue to know when the database was last updated. * Ensure we only return SensorLocations with a country * Ft increase gurnicorn timeout (#92) * Increase Gunicorn timeout to 3 minutes * Return sorted sensors locations * Includes country in the sensor data serializer (#93) * Includes country in the sensor data serializer * Only return location id and country * Ft add location filter (#95) * Adds a nodes filter using location * Adds nodes filter for v1 of the API * Remove filterling on v2 since it's only used for the F.E map * Use default django filter backend * Makes country location matching case insensitive. * [Ft] Nodes last notify (#94) * update node last_notify * update nodeserializer * import from feinstaub * class naming * code refactor * remove repeating line * comment line * Update sensorsafrica/api/v1/serializers.py Co-authored-by: _ Kilemensi <kilemensi@users.noreply.github.com> * rename verbose to lastnotify * update only when current notify is less than sensordata timestamp Co-authored-by: _ Kilemensi <kilemensi@users.noreply.github.com> * Ft add location filter for /v2/data (#97) * Adds location filter for /data endpoint * Temporaliry Remove iexact * [FT] Time filter (#96) * add last_notify filter on nodes, sensorfilter * view class override * filter ovverides * add exact to look up exp * Includes missing import Co-authored-by: esir <esirkings@gmail.com> Co-authored-by: Khadija Mahanga <khadijamahanga@y7mail.com> Co-authored-by: _ Kilemensi <kilemensi@users.noreply.github.com>
1 parent c420e2f commit 1e14cb7

19 files changed

+742
-180
lines changed

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@ migrate:
2525
test:
2626
$(COMPOSE) exec api pytest --pylama
2727

28+
testexpr:
29+
$(COMPOSE) exec api pytest --pylama -k '$(expr)'
30+
2831
createsuperuser:
2932
$(COMPOSE) exec api python manage.py createsuperuser
3033

34+
down:
35+
$(COMPOSE) down
3136

3237
clean:
3338
@find . -name "*.pyc" -exec rm -rf {} \;

contrib/start.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ celery -A sensorsafrica flower --basic_auth=$SENSORSAFRICA_FLOWER_ADMIN_USERNAME
1616
echo Starting Gunicorn.
1717
exec gunicorn \
1818
--bind 0.0.0.0:8000 \
19-
--workers 3 \
19+
--timeout 180 \
20+
--workers 5 \
2021
--worker-class gevent \
2122
--log-level=info \
2223
--log-file=/src/logs/gunicorn.log \

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
django==1.11.27 #LTS
1+
django==1.11.29 #LTS
22
coreapi==2.3.3
33
dj-database-url==0.5.0
44
timeago==1.0.10
55

6-
flower==0.9.2
6+
flower==0.9.5
77
tornado<6
8-
sentry-sdk==0.7.3
8+
sentry-sdk==0.19.5
99
celery==4.2.1
1010
gevent==1.2.2
1111
greenlet==0.4.12

sensorsafrica/api/v1/filters.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import django_filters
2+
from django.db import models
3+
4+
from feinstaub.sensors.models import Node, SensorData
5+
6+
class NodeFilter(django_filters.FilterSet):
7+
class Meta:
8+
model = Node
9+
fields = {
10+
"location__country": ["exact"],
11+
"last_notify": ["exact", "gte", "lte"]}
12+
filter_overrides = {
13+
models.DateTimeField: {
14+
'filter_class': django_filters.IsoDateTimeFilter,
15+
},
16+
}
17+
18+
19+
class SensorFilter(django_filters.FilterSet):
20+
class Meta:
21+
model = SensorData
22+
fields = {
23+
"sensor": ["exact"],
24+
"timestamp": ["exact", "gte", "lte"]
25+
}
26+
filter_overrides = {
27+
models.DateTimeField: {
28+
'filter_class': django_filters.IsoDateTimeFilter,
29+
},
30+
}

sensorsafrica/api/v1/router.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
# The base version is entirely based on feinstaub
22
from feinstaub.main.views import UsersView
33
from feinstaub.sensors.views import (
4-
NodeView,
5-
NowView,
6-
PostSensorDataView,
74
SensorView,
85
StatisticsView,
96
SensorDataView,
107
)
118

12-
from .views import SensorDataView as SensorsAfricaSensorDataView, FilterView
9+
from .views import (
10+
FilterView,
11+
NodeView,
12+
NowView,
13+
PostSensorDataView,
14+
SensorsAfricaSensorDataView,
15+
VerboseSensorDataView,
16+
)
1317

1418
from rest_framework import routers
1519

1620
router = routers.DefaultRouter()
1721
router.register(r"push-sensor-data", PostSensorDataView)
1822
router.register(r"node", NodeView)
1923
router.register(r"sensor", SensorView)
20-
router.register(r"data", SensorDataView)
24+
router.register(r"data", VerboseSensorDataView)
2125
router.register(r"statistics", StatisticsView, basename="statistics")
2226
router.register(r"now", NowView)
2327
router.register(r"user", UsersView)
24-
router.register(r"sensors/(?P<sensor_id>\d+)",
25-
SensorsAfricaSensorDataView, basename="sensors")
28+
router.register(
29+
r"sensors/(?P<sensor_id>\d+)", SensorsAfricaSensorDataView, basename="sensors"
30+
)
2631
router.register(r"filter", FilterView, basename="filter")
2732

2833
api_urls = router.urls

sensorsafrica/api/v1/serializers.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
from rest_framework import serializers
22
from feinstaub.sensors.models import (
3+
Node,
34
SensorData,
45
SensorDataValue,
56
SensorLocation
67
)
8+
from feinstaub.sensors.serializers import (
9+
NestedSensorLocationSerializer,
10+
NestedSensorSerializer,
11+
SensorDataSerializer as PostSensorDataSerializer
12+
)
13+
14+
class NodeSerializer(serializers.ModelSerializer):
15+
sensors = NestedSensorSerializer(many=True)
16+
location = NestedSensorLocationSerializer()
717

18+
class Meta:
19+
model = Node
20+
fields = ('id', 'sensors', 'uid', 'owner', 'location', 'last_notify')
821

922
class SensorLocationSerializer(serializers.ModelSerializer):
1023
class Meta:
@@ -25,3 +38,19 @@ class SensorDataSerializer(serializers.ModelSerializer):
2538
class Meta:
2639
model = SensorData
2740
fields = ['location', 'timestamp', 'sensordatavalues']
41+
42+
class LastNotifySensorDataSerializer(PostSensorDataSerializer):
43+
44+
def create(self, validated_data):
45+
sd = super().create(validated_data)
46+
# use node from authenticator
47+
successful_authenticator = self.context['request'].successful_authenticator
48+
node, pin = successful_authenticator.authenticate(self.context['request'])
49+
50+
#sometimes we post historical data (eg: from other network)
51+
#this means we have to update last_notify only if current timestamp is greater than what's there
52+
if node.last_notify is None or node.last_notify < sd.timestamp:
53+
node.last_notify = sd.timestamp
54+
node.save()
55+
56+
return sd

sensorsafrica/api/v1/views.py

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,112 @@
11
import datetime
22
import pytz
33
import json
4+
import django_filters
45

5-
from rest_framework.exceptions import ValidationError
66

77
from django.conf import settings
8-
from django.utils import timezone
9-
from dateutil.relativedelta import relativedelta
108
from django.db.models import ExpressionWrapper, F, FloatField, Max, Min, Sum, Avg, Q
119
from django.db.models.functions import Cast, TruncDate
10+
from dateutil.relativedelta import relativedelta
11+
from django.utils import timezone
1212
from rest_framework import mixins, pagination, viewsets
13+
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
14+
from rest_framework.exceptions import ValidationError
15+
from rest_framework.permissions import IsAuthenticatedOrReadOnly
1316

14-
from .serializers import SensorDataSerializer
15-
from feinstaub.sensors.models import SensorData
17+
from feinstaub.sensors.models import Node, SensorData
18+
from feinstaub.sensors.serializers import NowSerializer
19+
from feinstaub.sensors.views import SensorDataView, StandardResultsSetPagination
20+
from feinstaub.sensors.authentication import NodeUidAuthentication
1621

22+
from .filters import NodeFilter, SensorFilter
23+
from .serializers import LastNotifySensorDataSerializer, NodeSerializer, SensorDataSerializer
1724

18-
class SensorDataView(mixins.ListModelMixin, viewsets.GenericViewSet):
25+
class FilterView(mixins.ListModelMixin, viewsets.GenericViewSet):
1926
serializer_class = SensorDataSerializer
2027

2128
def get_queryset(self):
29+
sensor_type = self.request.GET.get("type", r"\w+")
30+
country = self.request.GET.get("country", r"\w+")
31+
city = self.request.GET.get("city", r"\w+")
2232
return (
23-
SensorData.objects
24-
.filter(
33+
SensorData.objects.filter(
2534
timestamp__gte=timezone.now() - datetime.timedelta(minutes=5),
26-
sensor=self.kwargs["sensor_id"]
35+
sensor__sensor_type__uid__iregex=sensor_type,
36+
location__country__iregex=country,
37+
location__city__iregex=city,
2738
)
28-
.only('sensor', 'timestamp')
29-
.prefetch_related('sensordatavalues')
39+
.only("sensor", "timestamp")
40+
.prefetch_related("sensordatavalues")
3041
)
3142

3243

33-
class FilterView(mixins.ListModelMixin, viewsets.GenericViewSet):
44+
class NodeView(
45+
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
46+
):
47+
"""Show all nodes belonging to authenticated user"""
48+
49+
authentication_classes = [SessionAuthentication, TokenAuthentication]
50+
pagination_class = StandardResultsSetPagination
51+
permission_classes = [IsAuthenticatedOrReadOnly]
52+
queryset = SensorData.objects.none()
53+
serializer_class = NodeSerializer
54+
filter_class = NodeFilter
55+
56+
def get_queryset(self):
57+
if self.request.user.is_authenticated():
58+
if self.request.user.groups.filter(name="show_me_everything").exists():
59+
return Node.objects.all()
60+
61+
return Node.objects.filter(
62+
Q(owner=self.request.user)
63+
| Q(
64+
owner__groups__name__in=[
65+
g.name for g in self.request.user.groups.all()
66+
]
67+
)
68+
)
69+
70+
return Node.objects.none()
71+
72+
73+
class NowView(mixins.ListModelMixin, viewsets.GenericViewSet):
74+
"""Show all public sensors active in the last 5 minutes with newest value"""
75+
76+
permission_classes = []
77+
serializer_class = NowSerializer
78+
queryset = SensorData.objects.none()
79+
80+
def get_queryset(self):
81+
now = timezone.now()
82+
startdate = now - datetime.timedelta(minutes=5)
83+
return SensorData.objects.filter(
84+
sensor__public=True, modified__range=[startdate, now]
85+
)
86+
87+
class PostSensorDataView(mixins.CreateModelMixin,
88+
viewsets.GenericViewSet):
89+
""" This endpoint is to POST data from the sensor to the api.
90+
"""
91+
authentication_classes = (NodeUidAuthentication,)
92+
permission_classes = tuple()
93+
serializer_class = LastNotifySensorDataSerializer
94+
queryset = SensorData.objects.all()
95+
96+
97+
class VerboseSensorDataView(SensorDataView):
98+
filter_class = SensorFilter
99+
100+
class SensorsAfricaSensorDataView(mixins.ListModelMixin, viewsets.GenericViewSet):
34101
serializer_class = SensorDataSerializer
35102

36103
def get_queryset(self):
37-
sensor_type = self.request.GET.get('type', r'\w+')
38-
country = self.request.GET.get('country', r'\w+')
39-
city = self.request.GET.get('city', r'\w+')
40104
return (
41-
SensorData.objects
42-
.filter(
105+
SensorData.objects.filter(
43106
timestamp__gte=timezone.now() - datetime.timedelta(minutes=5),
44-
sensor__sensor_type__uid__iregex=sensor_type,
45-
location__country__iregex=country,
46-
location__city__iregex=city
107+
sensor=self.kwargs["sensor_id"],
47108
)
48-
.only('sensor', 'timestamp')
49-
.prefetch_related('sensordatavalues')
109+
.only("sensor", "timestamp")
110+
.prefetch_related("sensordatavalues")
50111
)
112+

sensorsafrica/api/v2/filters.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from feinstaub.sensors.views import SensorFilter
2+
3+
class CustomSensorFilter(SensorFilter):
4+
class Meta(SensorFilter.Meta):
5+
# Pick the fields already defined and add the location__country field
6+
fields = {**SensorFilter.Meta.fields, **{'location__country': ['exact']}}

sensorsafrica/api/v2/router.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,45 @@
11
from rest_framework import routers
22
from django.conf.urls import url, include
33

4-
from .views import SensorDataStatView, CityView, NodesView
4+
from .views import (
5+
CitiesView,
6+
NodesView,
7+
SensorDataStatsView,
8+
SensorDataView,
9+
SensorLocationsView,
10+
SensorTypesView,
11+
SensorsView,
12+
meta_data,
13+
)
14+
15+
stat_data_router = routers.DefaultRouter()
16+
stat_data_router.register(r"", SensorDataStatsView)
517

618
data_router = routers.DefaultRouter()
19+
data_router.register(r"", SensorDataView)
720

8-
data_router.register(r"", SensorDataStatView)
21+
cities_router = routers.DefaultRouter()
22+
cities_router.register(r"", CitiesView, basename="cities")
923

10-
city_router = routers.DefaultRouter()
24+
nodes_router = routers.DefaultRouter()
25+
nodes_router.register(r"", NodesView, basename="map")
1126

12-
city_router.register(r"", CityView)
27+
sensors_router = routers.DefaultRouter()
28+
sensors_router.register(r"", SensorsView, basename="sensors")
1329

14-
nodes_router = routers.DefaultRouter()
30+
sensor_locations_router = routers.DefaultRouter()
31+
sensor_locations_router.register(r"", SensorLocationsView, basename="locations")
1532

16-
nodes_router.register(r"", NodesView, basename="map")
33+
sensor_types_router = routers.DefaultRouter()
34+
sensor_types_router.register(r"", SensorTypesView, basename="sensor_types")
1735

1836
api_urls = [
19-
url(r"data/(?P<sensor_type>[air]+)/", include(data_router.urls)),
20-
url(r"cities/", include(city_router.urls)),
21-
url(r"nodes/", include(nodes_router.urls))
37+
url(r"data/(?P<sensor_type>[air]+)/", include(stat_data_router.urls)),
38+
url(r"data/", include(data_router.urls)),
39+
url(r"cities/", include(cities_router.urls)),
40+
url(r"nodes/", include(nodes_router.urls)),
41+
url(r"locations/", include(sensor_locations_router.urls)),
42+
url(r"sensors/", include(sensors_router.urls)),
43+
url(r"sensor-types/", include(sensor_types_router.urls)),
44+
url(r"meta/", meta_data),
2245
]

0 commit comments

Comments
 (0)