From 5789bf9b0bb3b059667f414f4649d45159b4c9ca Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 28 Sep 2023 20:07:04 +0300 Subject: [PATCH] Added envvars for ports; Added geojson calculated in DB --- .env.example | 25 ++++++++++++- docker-compose.dev.yml | 6 ++-- docker-compose.yml | 13 ++++--- src/django_oapif/decorators.py | 64 ++++++++++++++++++++++++++++------ src/django_oapif/functions.py | 20 +++++++++++ src/django_oapif/pagination.py | 13 +++++-- src/tests/settings.py | 8 ++--- src/tests/views.py | 0 8 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 src/django_oapif/functions.py delete mode 100644 src/tests/views.py diff --git a/.env.example b/.env.example index 1e70d644..cc541014 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,14 @@ OGCAPIF_HOST=localhost # Change this to some long and random sequence DJANGO_SECRET_KEY=_change_me_ -# Customize the postgres superuser password +# Postgres connection settings +POSTGRES_USER=postgres POSTGRES_PASSWORD=_change_me_ +POSTGRES_DB=postgres +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +# SSL mode. Most of the times should be either "prefer" OR "require". Default: "prefer" +POSTGRES_SSLMODE=prefer # Transifex token, required to pull translations TX_TOKEN=_change_me_ @@ -20,3 +26,20 @@ GEOMETRY_SRID=2056 # (cross-platform compatibility, do not change) COMPOSE_FILE_SEPARATOR=: + +# The Django development port. Not used in production. +# DEFAULT: 7180 +DJANGO_DEV_PORT=7180 + +# The Django development port. Not used in production. +# DEFAULT: 7178 +DEBUGPY_PORT=7178 + +# Caddy HTTP port. Default: 80 +WEB_HTTP_PORT=80 + +# Caddy HTTPS port. Default: 443 +WEB_HTTPS_PORT=443 + +# Postgres port on the host. Not used in production. Default: 7132 +HOST_POSTGRES_PORT=7132 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8fb533a5..64ce1bdc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -23,11 +23,11 @@ services: - ./unit_tests_outputs/:/unit_tests_outputs ports: # making django directly accessible for debugging - - 8000:8000 + - ${DJANGO_DEV_PORT:?}:8000 # debugpy - - 5678:5678 + - ${DEBUGPY_PORT:?}:5678 command: python3 -m debugpy --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 postgres: ports: - - 5432:5432 + - ${HOST_POSTGRES_PORT:?}:5432 diff --git a/docker-compose.yml b/docker-compose.yml index 2e83ae62..1de71cbc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,8 @@ services: environment: OGCAPIF_HOST: ${OGCAPIF_HOST:?} ports: - - 80:80 - - 443:443 + - ${WEB_HTTP_PORT:?}:80 + - ${WEB_HTTPS_PORT:?}:443 django: image: opengisch/django-oapif:latest @@ -40,14 +40,21 @@ services: DJANGO_STATIC_ROOT: /static_volume DJANGO_MEDIA_ROOT: /media_volume GEOMETRY_SRID: ${GEOMETRY_SRID:-2056} + POSTGRES_USER: ${POSTGRES_USER:?} + POSTGRES_DB: ${POSTGRES_DB:?} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?} + POSTGRES_HOST: ${POSTGRES_HOST:?} + POSTGRES_PORT: ${POSTGRES_PORT:?} + POSTGRES_SSLMODE: ${POSTGRES_SSLMODE:?} TX_TOKEN: ${TX_TOKEN} command: python3 manage.py runserver 0.0.0.0:8000 postgres: image: postgis/postgis:13-3.1 environment: + POSTGRES_USER: ${POSTGRES_USER:?} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?} + POSTGRES_DB: ${POSTGRES_DB:?} healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s @@ -55,8 +62,6 @@ services: retries: 5 # If you need the database to be accessible from the host # uncomment the two lines below - # ports: - # - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data diff --git a/src/django_oapif/decorators.py b/src/django_oapif/decorators.py index be6d5481..d550b939 100644 --- a/src/django_oapif/decorators.py +++ b/src/django_oapif/decorators.py @@ -1,7 +1,9 @@ from typing import Any, Callable, Dict, Optional -from django.db.models import Model -from rest_framework import viewsets +from django.db import models +from django.db.models.functions import Cast +from rest_framework import serializers, viewsets +from rest_framework.serializers import ModelSerializer from rest_framework_gis.serializers import GeoFeatureModelSerializer from django_oapif.metadata import OAPIFMetadata @@ -9,6 +11,10 @@ from django_oapif.urls import oapif_router from .filters import BboxFilterBackend +from .functions import AsGeoJSON + +USE_PG_GEOJSON = False +USE_PG_GEOJSON = True def register_oapif_viewset( @@ -16,7 +22,7 @@ def register_oapif_viewset( skip_geom: Optional[bool] = False, custom_serializer_attrs: Dict[str, Any] = None, custom_viewset_attrs: Dict[str, Any] = None, -) -> Callable[[Any], Model]: +) -> Callable[[Any], models.Model]: """ This decorator takes care of all boilerplate code (creating a serializer, a viewset and registering it) to register a model to the default OAPIF endpoint. @@ -47,13 +53,37 @@ def inner(Model): _viewset_oapif_geom_lookup = None _geo_field = None - class AutoSerializer(GeoFeatureModelSerializer): - class Meta: - nonlocal _geo_field - - model = Model - fields = "__all__" - geo_field = _geo_field + if USE_PG_GEOJSON: + + class AutoSerializer(ModelSerializer): + geojson = serializers.JSONField() + + class Meta: + model = Model + fields = [ + "id", + "geojson", + "field_0", + "field_1", + "field_2", + "field_3", + "field_4", + "field_5", + "field_6", + "field_7", + "field_8", + "field_9", + ] + + else: + + class AutoSerializer(GeoFeatureModelSerializer): + class Meta: + nonlocal _geo_field + + model = Model + fields = "__all__" + geo_field = _geo_field # Create the viewset class Viewset(OAPIFDescribeModelViewSetMixin, viewsets.ModelViewSet): @@ -84,6 +114,20 @@ def finalize_response(self, request, response, *args, **kwargs): response.headers["Allow"] = allowed_actions return response + def get_queryset(self): + qs = super().get_queryset() + + if USE_PG_GEOJSON: + # NOTE the defer should not be needed, as the field should be skipped already when we define `Serializer.Meta.Fields` without the `geom` col + qs = qs.defer("geom") + qs = qs.annotate( + geojson=Cast( + AsGeoJSON("geom", False, False), models.JSONField() + ) + ) + + return qs + # Apply custom serializer attributes for k, v in custom_serializer_attrs.items(): setattr(AutoSerializer.Meta, k, v) diff --git a/src/django_oapif/functions.py b/src/django_oapif/functions.py new file mode 100644 index 00000000..05c87df9 --- /dev/null +++ b/src/django_oapif/functions.py @@ -0,0 +1,20 @@ +from django.contrib.gis.db.models import functions +from django.db import models + + +class AsGeoJSON(functions.AsGeoJSON): + output_field = models.TextField() + + def __init__(self, expression, bbox=False, crs=False, precision=8, **extra): + expressions = [expression] + if precision is not None: + expressions.append(self._handle_param(precision, "precision", int)) + options = 0 + if crs and bbox: + options = 3 + elif bbox: + options = 1 + elif crs: + options = 2 + expressions.append(options) + super().__init__(*expressions, **extra) diff --git a/src/django_oapif/pagination.py b/src/django_oapif/pagination.py index 47469095..73e48280 100644 --- a/src/django_oapif/pagination.py +++ b/src/django_oapif/pagination.py @@ -1,6 +1,7 @@ from django.http import HttpResponse from rest_framework import pagination from rest_framework.response import Response +from rest_framework.utils.serializer_helpers import ReturnList class OapifPagination(pagination.LimitOffsetPagination): @@ -9,6 +10,14 @@ class OapifPagination(pagination.LimitOffsetPagination): default_limit = 1000 def get_paginated_response(self, data): + if isinstance(data, ReturnList): + number_returned = len(data) + + extra_params = {"features": [*data]} + else: + number_returned = len(data["features"]) + extra_params = {**data} + return Response( { "links": [ @@ -25,9 +34,9 @@ def get_paginated_response(self, data): "href": self.get_previous_link(), }, ], - "numberReturned": len(data["features"]), + "numberReturned": number_returned, "numberMatched": self.count, - **data, + **extra_params, } ) diff --git a/src/tests/settings.py b/src/tests/settings.py index 560f2f57..5d564b2c 100644 --- a/src/tests/settings.py +++ b/src/tests/settings.py @@ -88,10 +88,10 @@ DATABASES = { "default": { "ENGINE": "django.contrib.gis.db.backends.postgis", - "NAME": "postgres", - "HOST": "postgres", - "PORT": 5432, - "USER": "postgres", + "NAME": os.getenv("POSTGRES_DB"), + "HOST": os.getenv("POSTGRES_HOST"), + "PORT": os.getenv("POSTGRES_PORT"), + "USER": os.getenv("POSTGRES_USER"), "PASSWORD": os.getenv("POSTGRES_PASSWORD"), } } diff --git a/src/tests/views.py b/src/tests/views.py deleted file mode 100644 index e69de29b..00000000