From 4fc278c9eb6fbca8557adb4e52a97b1476bd4ff5 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Wed, 25 Sep 2024 09:53:56 +0300 Subject: [PATCH 01/13] chore: update Dockerfile and requirements, cleanup settings --- Dockerfile | 23 ++++++++++------------- README.md | 7 ++++++- docker-compose.yml | 30 +++++++++++++++++------------- requirements.txt | 2 +- sensorsafrica/settings.py | 7 ------- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0ccc619..66d9b7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,25 +2,21 @@ FROM python:3.6.3 ENV PYTHONUNBUFFERED 1 -# Create root directory for our project in the container -RUN mkdir /src - -# Create application subdirectories +# Create application root directory WORKDIR /src + RUN mkdir media static logs VOLUME [ "/src/logs" ] -# Copy the current directory contents into the container at sensorsafrica -ADD . /src/ - -# Upgrade pip and setuptools -RUN pip install -q -U pip setuptools +# Upgrade pip and setuptools with trusted hosts +RUN python -m pip install --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org --upgrade pip setuptools -# Install feinstaub from opendata-stuttgart -RUN pip install -q git+https://github.com/opendata-stuttgart/feinstaub-api +# Copy the current directory contents into the container at sensorsafrica +COPY . /src/ -# Install sensors.AFRICA-api and its dependencies -RUN pip install -q -U . +# Upgrade pip and setuptools, install dependencies +RUN pip install -q git+https://github.com/opendata-stuttgart/feinstaub-api && \ + pip install -q -U . # Expose port server EXPOSE 8000 @@ -31,3 +27,4 @@ COPY ./contrib/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD [ "/start.sh" ] + diff --git a/README.md b/README.md index 1f70360..deea24f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ API to save and access data from deployed sensors in cities all around Africa. ## Documentation -The API is documented [here.](https://github.com/CodeForAfricaLabs/sensors.AFRICA-api/wiki/API-Documentation) +The API is documented [here.](https://github.com/CodeForAfricaLabs/sensors.AFRICA-api/wiki/API-Documentation) ## Development @@ -29,6 +29,11 @@ GRANT ALL PRIVILEGES ON DATABASE sensorsafrica TO sensorsafrica; - Migrate the database; `python manage.py migrate` - Run the server; `python manage.py runserver` +- Create super user for admin login; `python manage.py createsuperuser` + + username: `` + email: blank + password: `` ### Docker diff --git a/docker-compose.yml b/docker-compose.yml index 49c12ef..8a1c12a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,15 +2,20 @@ version: '3.3' services: rabbitmq: - image: rabbitmq:3.5.1 + image: rabbitmq:3.12.7-management ports: - - 4369:4369 - - 5672:5672 - - 25672:25672 - - 15672:15672 + - "5672:5672" + # GUI port + - "15672:15672" environment: - - RABBITMQ_USERNAME=sensorsafrica - - RABBITMQ_PASSWORD=sensorsafrica + - RABBITMQ_DEFAULT_USER=sensorsafrica + - RABBITMQ_DEFAULT_PASS=sensorsafrica + healthcheck: + test: [ "CMD-SHELL", "rabbitmq-diagnostics -q ping" ] + interval: 10s + timeout: 5s + retries: 2 + postgres: image: postgres:13.7 ports: @@ -25,12 +30,11 @@ services: build: context: . environment: - SENSORSAFRICA_DATABASE_URL: postgres://sensorsafrica:sensorsafrica@postgres:5432/sensorsafrica - SENSORSAFRICA_READ_DATABASE_URLS: postgres://sensorsafrica:sensorsafrica@postgres:5432/sensorsafrica - SENSORSAFRICA_RABBITMQ_URL: amqp://sensorsafrica:sensorsafrica@rabbitmq// - SENSORSAFRICA_FLOWER_ADMIN_USERNAME: admin - SENSORSAFRICA_FLOWER_ADMIN_PASSWORD: password - DOKKU_APP_NAME: sensorsafrica + SENSORSAFRICA_DATABASE_URL: ${SENSORSAFRICA_DATABASE_URL:-postgres://sensorsafrica:sensorsafrica@postgres:5432/sensorsafrica} + SENSORSAFRICA_RABBITMQ_URL: ${SENSORSAFRICA_RABBITMQ_URL:-amqp://sensorsafrica:sensorsafrica@rabbitmq/} + SENSORSAFRICA_FLOWER_ADMIN_USERNAME: ${SENSORSAFRICA_FLOWER_ADMIN_USERNAME:-admin} + SENSORSAFRICA_FLOWER_ADMIN_PASSWORD: ${SENSORSAFRICA_FLOWER_ADMIN_PASSWORD:-password} + DOKKU_APP_NAME: ${DOKKU_APP_NAME:-sensorsafrica} depends_on: - postgres - rabbitmq diff --git a/requirements.txt b/requirements.txt index 92145ee..086b243 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ ckanapi==4.1 celery-slack==0.3.0 -urllib3<1.25,>=1.21.1 #requests 2.21.0 +urllib3<2 django-cors-headers==3.0.2 diff --git a/sensorsafrica/settings.py b/sensorsafrica/settings.py index fcbbf17..2377373 100644 --- a/sensorsafrica/settings.py +++ b/sensorsafrica/settings.py @@ -113,13 +113,6 @@ DATABASES = {"default": dj_database_url.parse(DATABASE_URL), } -READ_DATABASE_URLS = os.getenv("SENSORSAFRICA_READ_DATABASE_URLS", DATABASE_URL).split(",") - -for index, read_database_url in enumerate(READ_DATABASE_URLS,start=1): - DATABASES[f"read_replica_{index}"] = dj_database_url.parse(read_database_url) - -DATABASE_ROUTERS = ["sensorsafrica.router.ReplicaRouter", ] - # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators From eb5d88c1af7efa536a32d675be1cf84fcb393ce0 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Wed, 25 Sep 2024 09:54:18 +0300 Subject: [PATCH 02/13] chore: add /env template --- .env.template | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .env.template diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..84c2e1f --- /dev/null +++ b/.env.template @@ -0,0 +1,8 @@ +SENSORSAFRICA_CELERY_SLACK_WEBHOOK="" +SENSORSAFRICA_CELERY_SLACK_WEBHOOK_FAILURES_ONLY="" +SENSORSAFRICA_DATABASE_URL="" +SENSORSAFRICA_DEBUG="" +SENSORSAFRICA_FLOWER_ADMIN_PASSWORD="" +SENSORSAFRICA_FLOWER_ADMIN_USERNAME="" +SENSORSAFRICA_RABBITMQ_URL="" +SENSORSAFRICA_SENTRY_DSN="" From 95c3d6979768dbd5df928d44bdfc68d5ee6b4a41 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Thu, 3 Oct 2024 13:04:02 +0300 Subject: [PATCH 03/13] fix: fix error TypeError 'bool' object is not callable --- sensorsafrica/api/v1/views.py | 4 ++-- sensorsafrica/api/v2/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sensorsafrica/api/v1/views.py b/sensorsafrica/api/v1/views.py index 6c6a83a..182a446 100644 --- a/sensorsafrica/api/v1/views.py +++ b/sensorsafrica/api/v1/views.py @@ -54,7 +54,7 @@ class NodeView( filter_class = NodeFilter def get_queryset(self): - if self.request.user.is_authenticated(): + if self.request.user.is_authenticated: if self.request.user.groups.filter(name="show_me_everything").exists(): return Node.objects.all() @@ -92,7 +92,7 @@ class PostSensorDataView(mixins.CreateModelMixin, permission_classes = tuple() serializer_class = LastNotifySensorDataSerializer queryset = SensorData.objects.all() - + class VerboseSensorDataView(SensorDataView): filter_class = SensorFilter diff --git a/sensorsafrica/api/v2/views.py b/sensorsafrica/api/v2/views.py index a2a1758..f669a52 100644 --- a/sensorsafrica/api/v2/views.py +++ b/sensorsafrica/api/v2/views.py @@ -248,7 +248,7 @@ class SensorDataView( serializer_class = SensorDataSerializer def get_queryset(self): - if self.request.user.is_authenticated(): + if self.request.user.is_authenticated: if self.request.user.groups.filter(name="show_me_everything").exists(): return SensorData.objects.all() From 6d287841af91a4e89c1ee3444a2b9269e1854e43 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Fri, 4 Oct 2024 12:14:47 +0300 Subject: [PATCH 04/13] chore: add Now api endpoint, cleanup router --- sensorsafrica/api/v2/router.py | 40 ++++++++++------------------------ sensorsafrica/api/v2/views.py | 17 ++++++++++++++- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/sensorsafrica/api/v2/router.py b/sensorsafrica/api/v2/router.py index 46f4bc4..3ed91d4 100644 --- a/sensorsafrica/api/v2/router.py +++ b/sensorsafrica/api/v2/router.py @@ -4,6 +4,7 @@ from .views import ( CitiesView, NodesView, + NowView, SensorDataStatsView, SensorDataView, SensorLocationsView, @@ -12,34 +13,17 @@ meta_data, ) -stat_data_router = routers.DefaultRouter() -stat_data_router.register(r"", SensorDataStatsView) - -data_router = routers.DefaultRouter() -data_router.register(r"", SensorDataView) - -cities_router = routers.DefaultRouter() -cities_router.register(r"", CitiesView, basename="cities") - -nodes_router = routers.DefaultRouter() -nodes_router.register(r"", NodesView, basename="map") - -sensors_router = routers.DefaultRouter() -sensors_router.register(r"", SensorsView, basename="sensors") - -sensor_locations_router = routers.DefaultRouter() -sensor_locations_router.register(r"", SensorLocationsView, basename="locations") - -sensor_types_router = routers.DefaultRouter() -sensor_types_router.register(r"", SensorTypesView, basename="sensor_types") +router = routers.DefaultRouter() +router.register(r"data", SensorDataView, basename="sensor-data") +router.register(r"data/(?P[air]+)", SensorDataStatsView, basename="sensor-data-stats") +router.register(r"cities", CitiesView, basename="cities") +router.register(r"nodes", NodesView, basename="nodes") +router.register(r"now", NowView, basename="now") +router.register(r"locations", SensorLocationsView, basename="sensor-locations") +router.register(r"sensors", SensorsView, basename="sensors") +router.register(r"sensor-types", SensorTypesView, basename="sensor-types") api_urls = [ - url(r"data/(?P[air]+)/", include(stat_data_router.urls)), - url(r"data/", include(data_router.urls)), - url(r"cities/", include(cities_router.urls)), - url(r"nodes/", include(nodes_router.urls)), - url(r"locations/", include(sensor_locations_router.urls)), - url(r"sensors/", include(sensors_router.urls)), - url(r"sensor-types/", include(sensor_types_router.urls)), - url(r"meta/", meta_data), + url(r"^", include(router.urls)), + url(r"^meta/", meta_data), ] diff --git a/sensorsafrica/api/v2/views.py b/sensorsafrica/api/v2/views.py index f669a52..af0a1b0 100644 --- a/sensorsafrica/api/v2/views.py +++ b/sensorsafrica/api/v2/views.py @@ -23,7 +23,7 @@ from rest_framework.decorators import api_view, authentication_classes from feinstaub.sensors.views import SensorFilter, StandardResultsSetPagination - +from feinstaub.sensors.serializers import NowSerializer from feinstaub.sensors.models import ( Node, Sensor, @@ -471,3 +471,18 @@ def get_database_last_updated(): sensor_data_value = SensorDataValue.objects.latest('created') if sensor_data_value: return sensor_data_value.modified + + +class NowView(mixins.ListModelMixin, viewsets.GenericViewSet): + """Show all public sensors active in the last 5 minutes with newest value""" + + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [IsAuthenticated] + serializer_class = NowSerializer + + def get_queryset(self): + now = timezone.now() + startdate = now - datetime.timedelta(minutes=5) + return SensorData.objects.filter( + sensor__public=True, modified__range=[startdate, now] + ) From 46abd4362ed8b13015e32b7fbaec2a13b31568fc Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Mon, 7 Oct 2024 09:59:14 +0300 Subject: [PATCH 05/13] chore: update v1 route to only push sensors data, add statistics view --- .env.template | 2 -- sensorsafrica/api/v1/router.py | 6 +++++- sensorsafrica/api/v2/router.py | 2 ++ sensorsafrica/api/v2/views.py | 39 +++++++++++++++++++++++++++++++++- sensorsafrica/urls.py | 4 ++-- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/.env.template b/.env.template index 84c2e1f..9c920db 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,3 @@ -SENSORSAFRICA_CELERY_SLACK_WEBHOOK="" -SENSORSAFRICA_CELERY_SLACK_WEBHOOK_FAILURES_ONLY="" SENSORSAFRICA_DATABASE_URL="" SENSORSAFRICA_DEBUG="" SENSORSAFRICA_FLOWER_ADMIN_PASSWORD="" diff --git a/sensorsafrica/api/v1/router.py b/sensorsafrica/api/v1/router.py index c923328..74a49be 100644 --- a/sensorsafrica/api/v1/router.py +++ b/sensorsafrica/api/v1/router.py @@ -18,7 +18,6 @@ from rest_framework import routers router = routers.DefaultRouter() -router.register(r"push-sensor-data", PostSensorDataView) router.register(r"node", NodeView) router.register(r"sensor", SensorView) router.register(r"data", VerboseSensorDataView) @@ -31,3 +30,8 @@ router.register(r"filter", FilterView, basename="filter") api_urls = router.urls + +push_sensor_data_router = routers.DefaultRouter() +push_sensor_data_router.register(r"push-sensor-data", PostSensorDataView) + +push_sensor_data_urls = push_sensor_data_router.urls diff --git a/sensorsafrica/api/v2/router.py b/sensorsafrica/api/v2/router.py index 3ed91d4..1396151 100644 --- a/sensorsafrica/api/v2/router.py +++ b/sensorsafrica/api/v2/router.py @@ -10,6 +10,7 @@ SensorLocationsView, SensorTypesView, SensorsView, + StatisticsView, meta_data, ) @@ -22,6 +23,7 @@ router.register(r"locations", SensorLocationsView, basename="sensor-locations") router.register(r"sensors", SensorsView, basename="sensors") router.register(r"sensor-types", SensorTypesView, basename="sensor-types") +router.register(r"statistics", StatisticsView, basename="statistics") api_urls = [ url(r"^", include(router.urls)), diff --git a/sensorsafrica/api/v2/views.py b/sensorsafrica/api/v2/views.py index af0a1b0..8de4b4d 100644 --- a/sensorsafrica/api/v2/views.py +++ b/sensorsafrica/api/v2/views.py @@ -9,7 +9,7 @@ from django.conf import settings from django.utils import timezone from django.db import connection -from django.db.models import ExpressionWrapper, F, FloatField, Max, Min, Sum, Avg, Q +from django.db.models import ExpressionWrapper, F, FloatField, Max, Min, Sum, Avg, Q, Count from django.db.models.functions import Cast, TruncHour, TruncDay, TruncMonth from django.utils.decorators import method_decorator from django.utils.text import slugify @@ -486,3 +486,40 @@ def get_queryset(self): return SensorData.objects.filter( sensor__public=True, modified__range=[startdate, now] ) + + +class StatisticsView(viewsets.ViewSet): + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [IsAuthenticated] + + def list(self, request): + user_count = User.objects.aggregate(count=Count('id'))['count'] + sensor_count = Sensor.objects.aggregate(count=Count('id'))['count'] + sensor_data_count = SensorData.objects.aggregate(count=Count('id'))['count'] + sensor_data_value_count = SensorDataValue.objects.aggregate(count=Count('id'))['count'] + sensor_type_count = SensorType.objects.aggregate(count=Count('id'))['count'] + sensor_type_list = list(SensorType.objects.order_by('uid').values_list('name', flat=True)) + location_count = SensorLocation.objects.aggregate(count=Count('id'))['count'] + + stats = { + 'user': { + 'count': user_count, + }, + 'sensor': { + 'count': sensor_count, + }, + 'sensor_data': { + 'count': sensor_data_count, + }, + 'sensor_data_value': { + 'count': sensor_data_value_count, + }, + 'sensor_type': { + 'count': sensor_type_count, + 'list': sensor_type_list, + }, + 'location': { + 'count': location_count, + } + } + return Response(stats) diff --git a/sensorsafrica/urls.py b/sensorsafrica/urls.py index 4e9f7d5..c6a23ef 100644 --- a/sensorsafrica/urls.py +++ b/sensorsafrica/urls.py @@ -21,13 +21,13 @@ from rest_framework.authtoken.views import obtain_auth_token from rest_framework.documentation import include_docs_urls -from .api.v1.router import api_urls as sensors_api_v1 +from .api.v1.router import push_sensor_data_urls from .api.v2.router import api_urls as sensors_api_v2 urlpatterns = [ url(r"^$", RedirectView.as_view(url="/docs/", permanent=False)), url(r"^admin/", admin.site.urls), - url(r"^v1/", include(sensors_api_v1)), + url(r"^v1/push-sensor-data/", include(push_sensor_data_urls)), url(r"^v2/", include(sensors_api_v2)), url(r"^get-auth-token/", obtain_auth_token), url(r"^auth/", include("rest_framework.urls", namespace="rest_framework")), From bc82a90b14a5edb84cb248197942cf91c223e6ef Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Tue, 22 Oct 2024 14:49:18 +0300 Subject: [PATCH 06/13] update: fix push-sensors-url --- sensorsafrica/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sensorsafrica/urls.py b/sensorsafrica/urls.py index c6a23ef..e8df713 100644 --- a/sensorsafrica/urls.py +++ b/sensorsafrica/urls.py @@ -27,7 +27,7 @@ urlpatterns = [ url(r"^$", RedirectView.as_view(url="/docs/", permanent=False)), url(r"^admin/", admin.site.urls), - url(r"^v1/push-sensor-data/", include(push_sensor_data_urls)), + url(r"^v1/", include(push_sensor_data_urls)), url(r"^v2/", include(sensors_api_v2)), url(r"^get-auth-token/", obtain_auth_token), url(r"^auth/", include("rest_framework.urls", namespace="rest_framework")), From a2f06d8363c24e03ac270a53fa7ad52c2bd37794 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Tue, 22 Oct 2024 14:49:41 +0300 Subject: [PATCH 07/13] update: update NodesView --- sensorsafrica/api/v2/views.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/sensorsafrica/api/v2/views.py b/sensorsafrica/api/v2/views.py index 8de4b4d..5f57483 100644 --- a/sensorsafrica/api/v2/views.py +++ b/sensorsafrica/api/v2/views.py @@ -17,6 +17,7 @@ from rest_framework import mixins, pagination, viewsets from rest_framework.authentication import SessionAuthentication, TokenAuthentication +from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny @@ -129,6 +130,7 @@ class CitiesView(mixins.ListModelMixin, viewsets.GenericViewSet): class NodesView(viewsets.ViewSet): + """Create and list nodes, with the option to list authenticated user's nodes.""" authentication_classes = [SessionAuthentication, TokenAuthentication] def get_permissions(self): @@ -139,7 +141,9 @@ def get_permissions(self): return [permission() for permission in permission_classes] - def list(self, request): + @action(detail=False, methods=["get"], url_path="list-nodes", url_name="list_nodes") + def list_nodes(self, request): + """List all public nodes with active sensors.""" nodes = [] # Loop through the last active nodes for last_active in LastActiveNodes.objects.iterator(): @@ -219,7 +223,17 @@ def list(self, request): return Response(nodes) - def create(self, request): + @action(detail=False, methods=["get"], url_path="my-nodes", url_name="my_nodes") + def list_my_nodes(self, request): + """List only the nodes owned by the authenticated user.""" + if request.user.is_authenticated: + queryset = Node.objects.filter(owner=request.user) + serializer = NodeSerializer(queryset, many=True) + return Response(serializer.data) + return Response({"detail": "Authentication credentials were not provided."}, status=403) + + @action(detail=False, methods=["post"], url_path="create-node", url_name="create_node") + def create_node(self, request): serializer = NodeSerializer(data=request.data) if serializer.is_valid(): serializer.save() From 72e30caab444c4d8ef3ca6dee59aafd5dd2bb059 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Tue, 22 Oct 2024 15:21:26 +0300 Subject: [PATCH 08/13] update: update NodesView --- sensorsafrica/api/v2/views.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sensorsafrica/api/v2/views.py b/sensorsafrica/api/v2/views.py index 5f57483..c9843eb 100644 --- a/sensorsafrica/api/v2/views.py +++ b/sensorsafrica/api/v2/views.py @@ -227,13 +227,20 @@ def list_nodes(self, request): def list_my_nodes(self, request): """List only the nodes owned by the authenticated user.""" if request.user.is_authenticated: - queryset = Node.objects.filter(owner=request.user) + queryset = Node.objects.filter( + Q(owner=request.user) + | Q( + owner__groups__name__in=[ + g.name for g in request.user.groups.all() + ] + ) + ) serializer = NodeSerializer(queryset, many=True) return Response(serializer.data) return Response({"detail": "Authentication credentials were not provided."}, status=403) - @action(detail=False, methods=["post"], url_path="create-node", url_name="create_node") - def create_node(self, request): + @action(detail=False, methods=["post"], url_path="register-node", url_name="register_node") + def register_node(self, request): serializer = NodeSerializer(data=request.data) if serializer.is_valid(): serializer.save() From be4d4cbd2bca9045a107844f0586dd146f805a07 Mon Sep 17 00:00:00 2001 From: gideon maina Date: Wed, 23 Oct 2024 17:21:43 +0300 Subject: [PATCH 09/13] Update README docker setup --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index deea24f..5f7cd95 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ GRANT ALL PRIVILEGES ON DATABASE sensorsafrica TO sensorsafrica; Using docker compose: +- Create a `.env` file using `.env.template` . ***docker-compose has some default values for these variables*** - Build the project; `docker-compose build` or `make build` - Run the project; `docker-compose up -d` or `make up` From cca5ad2db421a24d3ff9d722aa3e341acfdcded2 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Thu, 24 Oct 2024 09:22:23 +0300 Subject: [PATCH 10/13] update: modify NodeView permissions --- sensorsafrica/api/v2/views.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/sensorsafrica/api/v2/views.py b/sensorsafrica/api/v2/views.py index c9843eb..2bdc230 100644 --- a/sensorsafrica/api/v2/views.py +++ b/sensorsafrica/api/v2/views.py @@ -132,14 +132,7 @@ class CitiesView(mixins.ListModelMixin, viewsets.GenericViewSet): class NodesView(viewsets.ViewSet): """Create and list nodes, with the option to list authenticated user's nodes.""" authentication_classes = [SessionAuthentication, TokenAuthentication] - - def get_permissions(self): - if self.action == "create": - permission_classes = [IsAuthenticated] - else: - permission_classes = [AllowAny] - - return [permission() for permission in permission_classes] + permission_classes = [IsAuthenticated] @action(detail=False, methods=["get"], url_path="list-nodes", url_name="list_nodes") def list_nodes(self, request): From 5488c91b716cbf0430bdd99628a88972ae11c2b3 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Tue, 29 Oct 2024 08:42:48 +0300 Subject: [PATCH 11/13] chore: update views docstrings --- sensorsafrica/api/v2/views.py | 49 ++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/sensorsafrica/api/v2/views.py b/sensorsafrica/api/v2/views.py index 2bdc230..caca2f2 100644 --- a/sensorsafrica/api/v2/views.py +++ b/sensorsafrica/api/v2/views.py @@ -251,7 +251,17 @@ class SensorDataPagination(pagination.CursorPagination): class SensorDataView( mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet ): - """This endpoint is to download sensor data from the api.""" + """ + View for retrieving and downloading detailed sensor data records, with access controlled based on + user permissions and ownership. + + This endpoint allows authenticated users to retrieve sensor data records, with the following access rules: + - Users in the `show_me_everything` group have access to all sensor data records. + - Other users can access data from sensors they own, sensors owned by members of their groups, or public sensors. + - Non-authenticated users can only access public sensor data. + """ + + authentication_classes = [SessionAuthentication, TokenAuthentication] queryset = SensorData.objects.all() @@ -279,9 +289,40 @@ def get_queryset(self): class SensorDataStatsView(mixins.ListModelMixin, viewsets.GenericViewSet): + """ + View to retrieve summarized statistics for specific sensor types (e.g., air quality) within a defined date range, + filtered by city and grouped by specified intervals (hourly, daily, or monthly). + + **URL Parameters:** + - `sensor_type` (str): The type of sensor data to retrieve (e.g., air_quality). + + **Query Parameters:** + - `city` (str, optional): Comma-separated list of city slugs to filter data by location. + - `from` (str, optional): Start date in "YYYY-MM-DD" format. Required if `to` is specified. + - `to` (str, optional): End date in "YYYY-MM-DD" format. Defaults to 24 hours before `to_date` if unspecified. + - `interval` (str, optional): Aggregation interval for results - either "hour", "day", or "month". Defaults to "day". + - `value_type` (str, optional): Comma-separated list of value types to filter (e.g., "PM2.5, PM10"). + + **Caching:** + - Results are cached for 1 hour (`@cache_page(3600)`) to reduce server load. + + **Returns:** + - A list of sensor data statistics, grouped by city, value type, and specified interval. + - Each entry includes: + - `value_type` (str): Type of sensor value (e.g., PM2.5). + - `city_slug` (str): City identifier. + - `truncated_timestamp` (datetime): Timestamp truncated to the specified interval. + - `start_datetime` (datetime): Start of the aggregated time period. + - `end_datetime` (datetime): End of the aggregated time period. + - `calculated_average` (float): Weighted average of sensor values. + - `calculated_minimum` (float): Minimum recorded value within the period. + - `calculated_maximum` (float): Maximum recorded value within the period. + """ queryset = SensorDataStat.objects.none() serializer_class = SensorDataStatSerializer pagination_class = CustomPagination + authentication_classes = [SessionAuthentication, TokenAuthentication] + permission_classes = [IsAuthenticated] @method_decorator(cache_page(3600)) def dispatch(self, request, *args, **kwargs): @@ -367,6 +408,9 @@ def get_queryset(self): class SensorLocationsView(viewsets.ViewSet): + """ + View for retrieving and creating sensor entries. + """ authentication_classes = [SessionAuthentication, TokenAuthentication] permission_classes = [IsAuthenticated] pagination_class = StandardResultsSetPagination @@ -386,6 +430,9 @@ def create(self, request): class SensorTypesView(viewsets.ViewSet): + """ + View for retrieving and creating sensor type entries. + """ authentication_classes = [SessionAuthentication, TokenAuthentication] permission_classes = [IsAuthenticated] pagination_class = StandardResultsSetPagination From 205af418ca4a1b49bfb5e0ccf1a3cfbf2a68e1f8 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Tue, 29 Oct 2024 08:43:21 +0300 Subject: [PATCH 12/13] chore: modify data stats url --- sensorsafrica/api/v2/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sensorsafrica/api/v2/router.py b/sensorsafrica/api/v2/router.py index 1396151..2edb377 100644 --- a/sensorsafrica/api/v2/router.py +++ b/sensorsafrica/api/v2/router.py @@ -16,7 +16,7 @@ router = routers.DefaultRouter() router.register(r"data", SensorDataView, basename="sensor-data") -router.register(r"data/(?P[air]+)", SensorDataStatsView, basename="sensor-data-stats") +router.register(r"data/stats/(?P[air]+)", SensorDataStatsView, basename="sensor-data-stats") router.register(r"cities", CitiesView, basename="cities") router.register(r"nodes", NodesView, basename="nodes") router.register(r"now", NowView, basename="now") From 893907d54646dcac244cbc4cad150c249ee32ee6 Mon Sep 17 00:00:00 2001 From: Xavier Frankline Date: Thu, 14 Nov 2024 15:21:30 +0300 Subject: [PATCH 13/13] chore: add .dockerignore file --- .dockerignore | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 4 +-- 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..69a711c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,77 @@ +# Git +.git +.gitignore + +# Docker +docker-compose.yml +.docker + +# Byte-compiled / optimized / DLL files +__pycache__/ +*/__pycache__/ +*/*/__pycache__/ +*/*/*/__pycache__/ +*.py[cod] +*/*.py[cod] +*/*/*.py[cod] +*/*/*/*.py[cod] + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +.env/ +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +*/.ropeproject +*/*/.ropeproject +*/*/*/.ropeproject + +# Vim swap files +*.swp +*/*.swp +*/*/*.swp +*/*/*/*.swp diff --git a/Dockerfile b/Dockerfile index 66d9b7e..aaef287 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,9 +14,9 @@ RUN python -m pip install --trusted-host pypi.python.org --trusted-host files.py # Copy the current directory contents into the container at sensorsafrica COPY . /src/ -# Upgrade pip and setuptools, install dependencies +# Install dependencies RUN pip install -q git+https://github.com/opendata-stuttgart/feinstaub-api && \ - pip install -q -U . + pip install -q . # Expose port server EXPOSE 8000