diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..b35346c4 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,80 @@ +name: Benchmark + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + benchmark: + runs-on: ubuntu-latest + env: + DOCKER_BUILDKIT: 1 + COMPOSE_DOCKER_CLI_BUILD: 1 + + steps: + - uses: actions/checkout@v4 + + - name: Create env + run: cp .env.example .env + + - name: Load .env file + uses: xom9ikk/dotenv@v2 + + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + cache: 'pip' + + - name: Install deps + run: | + sudo apt-get install -y jq + pip install plotly kaleido + # hyperfine for timing + HF_VERSION=1.18.0 + wget https://github.com/sharkdp/hyperfine/releases/download/v${HF_VERSION}/hyperfine_${HF_VERSION}_amd64.deb + sudo dpkg -i hyperfine_${HF_VERSION}_amd64.deb + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Django image + uses: docker/build-push-action@v5 + with: + context: . + provenance: false + file: docker/django/Dockerfile + pull: true + cache-from: type=registry,ref=opengisch/django-oapif:latest + tags: opengisch/django-oapif:latest + + - name: Download fixtures + run: ./scripts/download-fixtures.sh + + - name: Start Django + run: ./scripts/restart.sh + + - name: Run Benchmark + run: ./tests/benchmark/time.sh + + - name: Create plots + run: ./tests/benchmark/plot.py + + - uses: actions/upload-artifact@v3 + with: + path: | + tests/benchmark/results/benchmark.dat + tests/benchmark/results/*.png + if-no-files-found: error + + - name: Failure logs + if: failure() + run: docker-compose logs diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 325cf191..36e432ea 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -14,7 +14,6 @@ jobs: env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 - OGCAPIF_HOST: localhost permissions: contents: write pull-requests: write # to write comment @@ -22,6 +21,12 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Create env + run: cp .env.example .env + + - name: Load .env file + uses: xom9ikk/dotenv@v2 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -58,7 +63,7 @@ jobs: docker compose exec django python manage.py populate_data - name: Healthcheck - run: wget --no-check-certificate https://localhost/oapif/collections/tests.point_2056_10fields/items + run: wget http://${OGCAPIF_HOST}:${DJANGO_DEV_PORT}/oapif/collections/tests.point_2056_10fields/items - name: Run conformance test suite run: docker compose run conformance_test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0b64664..a4ac94ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,11 +18,16 @@ jobs: env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 - OGCAPIF_HOST: localhost steps: - uses: actions/checkout@v4 + - name: Create env + run: cp .env.example .env + + - name: Load .env file + uses: xom9ikk/dotenv@v2 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -53,8 +58,7 @@ jobs: # start the stack docker compose --profile testing_integration up --build -d - # deploy static files and migrate database - docker compose exec django python manage.py collectstatic --no-input + # migrate database docker compose exec django python manage.py migrate --no-input docker compose exec django python manage.py populate_users docker compose exec django python manage.py populate_data diff --git a/.gitignore b/.gitignore index 1ee417ed..506dde15 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,14 @@ dist *.egg-info src/django_oapif/__version__.py +tests/benchmark/results + +src/tests/fixtures/polygon_2056.json.gz +src/tests/fixtures/polygon_2056_local_geom.json.gz + +data static media test_outputs .vscode/ +.python-version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b37d9ac2..bbf84f0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,5 @@ +exclude: ^src/tests/migrations/ + repos: # Fix end of files - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/docker-compose.arm64.yml b/docker-compose.arm64.yml new file mode 100644 index 00000000..fdd7f7ca --- /dev/null +++ b/docker-compose.arm64.yml @@ -0,0 +1,7 @@ + +# see https://github.com/postgis/docker-postgis/issues/216#issuecomment-1763399631 +# https://github.com/postgis/docker-postgis/pull/356 + +services: + postgres: + image: imresamu/postgis-arm64:15-3.4-alpine diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 64ce1bdc..d3f92c65 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,4 +1,4 @@ -# overrides for local developpement (mounts the source, enables debug, live-reload, etc.) +# overrides for local development (mounts the source, enables debug, live-reload, etc.) # DO NOT USE ON PRODUCTION !!! version: '3.7' diff --git a/docker/django/Dockerfile b/docker/django/Dockerfile index 59519fba..c586c21a 100644 --- a/docker/django/Dockerfile +++ b/docker/django/Dockerfile @@ -20,7 +20,7 @@ RUN apt-get update && \ COPY ./requirements.txt . RUN pip install -r requirements.txt -# install dev depenencies +# install dev dependencies ARG DEV=false COPY ./requirements-dev.txt . RUN if [ "$DEV" = "true" ] ; then pip install -r requirements-dev.txt ; fi diff --git a/scripts/download-fixtures.sh b/scripts/download-fixtures.sh new file mode 100755 index 00000000..42d55a07 --- /dev/null +++ b/scripts/download-fixtures.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +mkdir -p src/tests/fixtures + +# download fixtures +curl -L -o ./src/tests/fixtures/polygon_2056.json.gz 'https://drive.google.com/uc?export=download&id=1UuGoK_9Y99jiTvQd4juxu85eVZhvlAvy' +curl -L -o ./src/tests/fixtures/polygon_2056_local_geom.json.gz 'https://drive.google.com/uc?export=download&id=18PGtiptcJiRtLnVq7N64EVQLagse0bu0' diff --git a/scripts/populate.sh b/scripts/populate.sh new file mode 100755 index 00000000..81062607 --- /dev/null +++ b/scripts/populate.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e +set -x + +SIZE=${1:-100} + + +docker compose exec django python manage.py flush --no-input + +docker compose exec django python manage.py populate_users +docker compose exec django python manage.py populate_data -s ${SIZE} + +docker compose exec django python manage.py loaddata polygon_2056 polygon_2056_local_geom diff --git a/scripts/restart.sh b/scripts/restart.sh index cbe0efa9..0432a1d8 100755 --- a/scripts/restart.sh +++ b/scripts/restart.sh @@ -4,16 +4,8 @@ set -e SIZE=${1:-100} -rm src/tests/migrations/0*.py || true docker compose down --volumes || true - docker compose up --build --force-recreate -d -sleep 5 -docker compose exec django python manage.py makemigrations docker compose exec django python manage.py migrate - -docker compose exec django python manage.py collectstatic --no-input -docker compose exec django python manage.py populate_users -docker compose exec django python manage.py populate_data -s ${SIZE} diff --git a/src/django_oapif/decorators.py b/src/django_oapif/decorators.py index ad295ed7..ef881ee3 100644 --- a/src/django_oapif/decorators.py +++ b/src/django_oapif/decorators.py @@ -54,12 +54,12 @@ def inner(Model): if serialize_geom_in_db and geom_field: class AutoSerializer(GeoFeatureModelSerializer): - _geom_geosjon = serializers.JSONField(required=False, allow_null=True, read_only=True) + _geom_geojson = serializers.JSONField(required=False, allow_null=True, read_only=True) class Meta: model = Model exclude = [geom_field] - geo_field = "_geom_geosjon" + geo_field = "_geom_geojson" def to_internal_value(self, data): # TODO: this needs improvement!!! @@ -127,7 +127,7 @@ def get_queryset(self): qs = super().get_queryset() if serialize_geom_in_db and geom_field: - qs = qs.annotate(_geom_geosjon=Cast(AsGeoJSON(geom_field, False, False), models.JSONField())) + qs = qs.annotate(_geom_geojson=Cast(AsGeoJSON(geom_field, False, False), models.JSONField())) return qs diff --git a/src/tests/management/commands/populate_data.py b/src/tests/management/commands/populate_data.py index 4fa0544f..96867bd7 100644 --- a/src/tests/management/commands/populate_data.py +++ b/src/tests/management/commands/populate_data.py @@ -1,6 +1,7 @@ import math import random import string +from copy import deepcopy from django.core.management import call_command from django.core.management.base import BaseCommand @@ -8,9 +9,12 @@ from tests.models import ( Line_2056_10fields, + Line_2056_10fields_local_geom, NoGeom_10fields, + NoGeom_100fields, Point_2056_10fields, - Point_2056_10fields_local_json, + Point_2056_10fields_local_geom, + SecretLayer, ) @@ -18,7 +22,7 @@ class Command(BaseCommand): help = "Populate db with testdata" def add_arguments(self, parser): - parser.add_argument("-s", "--size", type=int, default=10000) + parser.add_argument("-s", "--size", type=int, default=1000) @transaction.atomic def handle(self, *args, **options): @@ -31,9 +35,12 @@ def handle(self, *args, **options): magnitude = math.ceil(math.sqrt(size)) points = [] - points_local_json = [] + points_local_geom = [] + secret_points = [] lines = [] + lines_local_geom = [] no_geoms = [] + no_geoms_100fields = [] letters = string.ascii_lowercase @@ -42,30 +49,48 @@ def handle(self, *args, **options): x = x_start + dx * step y = y_start + dy * step geom_pt_wkt = f"Point({x:4f} {y:4f})" - geom_line_wkt = f"LineString({x:4f} {y:4f}, {x+random.randint(10,50):4f} {y+random.randint(10,50):4f})" - - fields = {} + geom_line_wkt = ( + f"LineString(" + f"{x:4f} {y:4f}, " + f"{x+random.randint(10,50):4f} {y+random.randint(10,50):4f})" + f"{x+random.randint(10,50):4f} {y+random.randint(10,50):4f})" + ) + + fields = {"field_int": random.randint(1, 999)} for f in range(10): - fields[f"field_{f}"] = "".join(random.choice(letters) for i in range(10)) + fields[f"field_str_{f}"] = "".join(random.choice(letters) for i in range(10)) no_geom = NoGeom_10fields(**fields) no_geoms.append(no_geom) + no_geom_100fields = deepcopy(fields) + for f in range(90): + no_geom_100fields[f"field_str_{10+f}"] = "".join(random.choice(letters) for i in range(10)) + no_geom_100fields = NoGeom_100fields(**no_geom_100fields) + no_geoms_100fields.append(no_geom_100fields) + fields["geom"] = geom_pt_wkt point = Point_2056_10fields(**fields) points.append(point) - point_local_json = Point_2056_10fields_local_json(**fields) - points_local_json.append(point_local_json) + point_local_geom = Point_2056_10fields_local_geom(**fields) + points_local_geom.append(point_local_geom) + secret_point = SecretLayer(**fields) + secret_points.append(secret_point) fields["geom"] = geom_line_wkt line = Line_2056_10fields(**fields) lines.append(line) + line_local_geom = Line_2056_10fields_local_geom(**fields) + lines_local_geom.append(line_local_geom) # Create objects in batches - Point_2056_10fields.objects.bulk_create(points) - Point_2056_10fields_local_json.objects.bulk_create(points_local_json) - NoGeom_10fields.objects.bulk_create(no_geoms) - Line_2056_10fields.objects.bulk_create(lines) + Point_2056_10fields.objects.bulk_create(points, batch_size=10000) + Point_2056_10fields_local_geom.objects.bulk_create(points_local_geom, batch_size=10000) + SecretLayer.objects.bulk_create(secret_points, batch_size=10000) + NoGeom_10fields.objects.bulk_create(no_geoms, batch_size=10000) + NoGeom_100fields.objects.bulk_create(no_geoms_100fields, batch_size=10000) + Line_2056_10fields.objects.bulk_create(lines, batch_size=10000) + Line_2056_10fields_local_geom.objects.bulk_create(lines_local_geom, batch_size=10000) # Call 'update_data' to update computed properties call_command("updatedata") diff --git a/src/tests/management/commands/populate_users.py b/src/tests/management/commands/populate_users.py index e4982814..0b9d1ae7 100644 --- a/src/tests/management/commands/populate_users.py +++ b/src/tests/management/commands/populate_users.py @@ -13,7 +13,17 @@ def handle(self, *args, **options): modifying = [] viewing = [] - for model in ("point_2056_10fields", "point_2056_10fields_local_json", "nogeom_10fields", "line_2056_10fields"): + for model in ( + "point_2056_10fields", + "point_2056_10fields_local_geom", + "nogeom_10fields", + "nogeom_100fields", + "line_2056_10fields", + "line_2056_10fields_local_geom", + "polygon_2056", + "polygon_2056_local_geom", + "secretlayer", + ): adding.append(Permission.objects.get(codename=f"add_{model}")) modifying.append(Permission.objects.get(codename=f"change_{model}")) viewing.append(Permission.objects.get(codename=f"view_{model}")) @@ -22,7 +32,7 @@ def handle(self, *args, **options): editors, _ = Group.objects.get_or_create(name="editors") viewers, _ = Group.objects.get_or_create(name="viewers") - viewers_wo_lines, _ = Group.objects.get_or_create(name="viewers_without_lines") + viewers_wo_secret, _ = Group.objects.get_or_create(name="viewers_without_secret") editors.save() viewers.save() @@ -31,17 +41,17 @@ def handle(self, *args, **options): viewers.permissions.set(viewing) viewer, _ = User.objects.get_or_create(username="demo_viewer") - viewer_wo_lines, _ = User.objects.get_or_create(username="demo_viewer_without_lines") + viewer_wo_secret, _ = User.objects.get_or_create(username="demo_viewer_without_secret") editor, _ = User.objects.get_or_create(username="demo_editor") super_user = User.objects.create_superuser(username="admin", is_staff=True) - for user in (viewer, viewer_wo_lines, editor, super_user): + for user in (viewer, viewer_wo_secret, editor, super_user): user.set_password("123") user.save() editor.groups.add(editors) viewer.groups.add(viewers) - viewer_wo_lines.groups.add(viewers_wo_lines) + viewer_wo_secret.groups.add(viewers_wo_secret) print( f"👥 added users 'demo_editor' & 'demo_viewer' to group 'editors' and 'viewers' respectively. Permissions set accordingly." diff --git a/src/tests/migrations/0001_initial.py b/src/tests/migrations/0001_initial.py index 02afb614..a7f2ff76 100644 --- a/src/tests/migrations/0001_initial.py +++ b/src/tests/migrations/0001_initial.py @@ -1,10 +1,8 @@ -# Generated by Django 4.2.5 on 2023-10-06 08:04 +# Generated by Django 4.2.6 on 2023-10-18 08:28 -import uuid - -import computedfields.resolver import django.contrib.gis.db.models.fields from django.db import migrations, models +import uuid class Migration(migrations.Migration): @@ -19,83 +17,258 @@ class Migration(migrations.Migration): name='Line_2056_10fields', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('field_bool', models.BooleanField(default=True)), + ('field_int', models.IntegerField(blank=True, null=True)), + ('field_str_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('geom', django.contrib.gis.db.models.fields.LineStringField(srid=2056, verbose_name='Geometry')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Line_2056_10fields_local_geom', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('field_bool', models.BooleanField(default=True)), + ('field_int', models.IntegerField(blank=True, null=True)), + ('field_str_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), ('geom', django.contrib.gis.db.models.fields.LineStringField(srid=2056, verbose_name='Geometry')), - ('field_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), - ('field_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), - ('field_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), - ('field_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), - ('field_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), - ('field_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), - ('field_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), - ('field_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), - ('field_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), - ('field_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), ], options={ 'abstract': False, }, - bases=(computedfields.resolver._ComputedFieldsModelBase, models.Model), + ), + migrations.CreateModel( + name='NoGeom_100fields', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('field_bool', models.BooleanField(default=True)), + ('field_int', models.IntegerField(blank=True, null=True)), + ('field_str_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_10', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_11', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_12', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_13', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_14', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_15', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_16', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_17', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_18', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_19', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_20', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_21', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_22', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_23', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_24', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_25', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_26', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_27', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_28', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_29', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_30', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_31', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_32', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_33', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_34', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_35', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_36', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_37', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_38', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_39', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_40', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_41', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_42', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_43', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_44', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_45', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_46', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_47', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_48', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_49', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_50', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_51', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_52', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_53', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_54', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_55', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_56', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_57', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_58', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_59', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_60', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_61', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_62', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_63', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_64', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_65', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_66', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_67', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_68', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_69', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_70', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_71', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_72', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_73', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_74', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_75', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_76', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_77', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_78', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_79', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_80', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_81', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_82', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_83', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_84', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_85', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_86', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_87', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_88', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_89', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_str_90', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_91', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_92', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_93', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_94', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_95', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_96', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_97', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_98', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_99', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='NoGeom_10fields', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('field_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), - ('field_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), - ('field_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), - ('field_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), - ('field_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), - ('field_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), - ('field_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), - ('field_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), - ('field_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), - ('field_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('field_bool', models.BooleanField(default=True)), + ('field_int', models.IntegerField(blank=True, null=True)), + ('field_str_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), ], options={ 'abstract': False, }, - bases=(computedfields.resolver._ComputedFieldsModelBase, models.Model), ), migrations.CreateModel( name='Point_2056_10fields', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('field_bool', models.BooleanField(default=True)), + ('field_int', models.IntegerField(blank=True, null=True)), + ('field_str_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), ('geom', django.contrib.gis.db.models.fields.PointField(srid=2056, verbose_name='Geometry')), - ('field_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), - ('field_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), - ('field_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), - ('field_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), - ('field_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), - ('field_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), - ('field_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), - ('field_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), - ('field_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), - ('field_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), ], options={ 'abstract': False, }, - bases=(computedfields.resolver._ComputedFieldsModelBase, models.Model), ), migrations.CreateModel( - name='Point_2056_10fields_local_json', + name='Point_2056_10fields_local_geom', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('field_bool', models.BooleanField(default=True)), + ('field_int', models.IntegerField(blank=True, null=True)), + ('field_str_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), + ('geom', django.contrib.gis.db.models.fields.PointField(srid=2056, verbose_name='Geometry')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Polygon_2056', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Name')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=2056, verbose_name='Geometry')), + ], + ), + migrations.CreateModel( + name='Polygon_2056_local_geom', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Name')), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(srid=2056, verbose_name='Geometry')), + ], + ), + migrations.CreateModel( + name='SecretLayer', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('field_bool', models.BooleanField(default=True)), + ('field_int', models.IntegerField(blank=True, null=True)), + ('field_str_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), + ('field_str_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), + ('field_str_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), + ('field_str_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), + ('field_str_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), + ('field_str_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), + ('field_str_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), + ('field_str_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), + ('field_str_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), + ('field_str_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), ('geom', django.contrib.gis.db.models.fields.PointField(srid=2056, verbose_name='Geometry')), - ('field_0', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 0')), - ('field_1', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 1')), - ('field_2', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 2')), - ('field_3', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 3')), - ('field_4', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 4')), - ('field_5', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 5')), - ('field_6', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 6')), - ('field_7', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 7')), - ('field_8', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 8')), - ('field_9', models.CharField(blank=True, max_length=255, null=True, verbose_name='Field 9')), ], options={ 'abstract': False, }, - bases=(computedfields.resolver._ComputedFieldsModelBase, models.Model), ), ] diff --git a/src/tests/models.py b/src/tests/models.py index 2473d736..4491cb22 100644 --- a/src/tests/models.py +++ b/src/tests/models.py @@ -1,7 +1,6 @@ import logging import uuid -from computedfields.models import ComputedFieldsModel from django.contrib.gis.db import models from django.utils.translation import gettext as _ from rest_framework import permissions @@ -11,67 +10,158 @@ logger = logging.getLogger(__name__) -@register_oapif_viewset(crs=2056) -class Point_2056_10fields(ComputedFieldsModel): +class BaseModelWithTenFields(models.Model): + class Meta: + abstract = True + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + field_bool = models.BooleanField(default=True) + field_int = models.IntegerField(null=True, blank=True) + field_str_0 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_1 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_2 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_3 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_4 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_5 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_6 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_7 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_8 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_9 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + + +@register_oapif_viewset(crs=2056) +class Point_2056_10fields(BaseModelWithTenFields): geom = models.PointField(srid=2056, verbose_name=_("Geometry")) - field_0 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) - field_1 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) - field_2 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) - field_3 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) - field_4 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) - field_5 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) - field_6 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) - field_7 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) - field_8 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) - field_9 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) @register_oapif_viewset(crs=2056, serialize_geom_in_db=False) -class Point_2056_10fields_local_json(ComputedFieldsModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) +class Point_2056_10fields_local_geom(BaseModelWithTenFields): geom = models.PointField(srid=2056, verbose_name=_("Geometry")) - field_0 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) - field_1 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) - field_2 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) - field_3 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) - field_4 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) - field_5 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) - field_6 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) - field_7 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) - field_8 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) - field_9 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) @register_oapif_viewset(geom_field=None) -class NoGeom_10fields(ComputedFieldsModel): +class NoGeom_10fields(BaseModelWithTenFields): + pass + + +@register_oapif_viewset(geom_field=None) +class NoGeom_100fields(BaseModelWithTenFields): + field_str_10 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_11 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_12 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_13 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_14 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_15 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_16 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_17 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_18 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_19 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + field_str_20 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_21 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_22 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_23 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_24 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_25 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_26 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_27 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_28 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_29 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + field_str_30 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_31 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_32 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_33 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_34 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_35 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_36 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_37 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_38 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_39 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + field_str_40 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_41 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_42 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_43 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_44 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_45 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_46 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_47 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_48 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_49 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + field_str_50 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_51 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_52 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_53 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_54 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_55 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_56 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_57 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_58 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_59 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + field_str_60 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_61 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_62 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_63 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_64 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_65 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_66 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_67 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_68 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_69 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + field_str_70 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_71 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_72 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_73 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_74 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_75 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_76 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_77 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_78 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_79 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + field_str_80 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_81 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_82 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_83 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_84 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_85 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_86 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_87 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_88 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_89 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + field_str_90 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) + field_str_91 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) + field_str_92 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) + field_str_93 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) + field_str_94 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) + field_str_95 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) + field_str_96 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) + field_str_97 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) + field_str_98 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) + field_str_99 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + + +@register_oapif_viewset(crs=2056) +class Line_2056_10fields(BaseModelWithTenFields): + geom = models.LineStringField(srid=2056, verbose_name=_("Geometry")) + + +@register_oapif_viewset(crs=2056, serialize_geom_in_db=False) +class Line_2056_10fields_local_geom(BaseModelWithTenFields): + geom = models.LineStringField(srid=2056, verbose_name=_("Geometry")) + + +@register_oapif_viewset(crs=2056) +class Polygon_2056(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - field_0 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) - field_1 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) - field_2 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) - field_3 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) - field_4 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) - field_5 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) - field_6 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) - field_7 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) - field_8 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) - field_9 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) - - -@register_oapif_viewset( - crs=2056, - custom_viewset_attrs={"permission_classes": (permissions.DjangoModelPermissions,)}, -) -class Line_2056_10fields(ComputedFieldsModel): + name = models.CharField(max_length=255, verbose_name=_("Name"), null=True, blank=True) + geom = models.MultiPolygonField(srid=2056, verbose_name=_("Geometry")) + + +@register_oapif_viewset(crs=2056, serialize_geom_in_db=False) +class Polygon_2056_local_geom(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - geom = models.LineStringField(srid=2056, verbose_name=_("Geometry")) - field_0 = models.CharField(max_length=255, verbose_name=_("Field 0"), null=True, blank=True) - field_1 = models.CharField(max_length=255, verbose_name=_("Field 1"), null=True, blank=True) - field_2 = models.CharField(max_length=255, verbose_name=_("Field 2"), null=True, blank=True) - field_3 = models.CharField(max_length=255, verbose_name=_("Field 3"), null=True, blank=True) - field_4 = models.CharField(max_length=255, verbose_name=_("Field 4"), null=True, blank=True) - field_5 = models.CharField(max_length=255, verbose_name=_("Field 5"), null=True, blank=True) - field_6 = models.CharField(max_length=255, verbose_name=_("Field 6"), null=True, blank=True) - field_7 = models.CharField(max_length=255, verbose_name=_("Field 7"), null=True, blank=True) - field_8 = models.CharField(max_length=255, verbose_name=_("Field 8"), null=True, blank=True) - field_9 = models.CharField(max_length=255, verbose_name=_("Field 9"), null=True, blank=True) + name = models.CharField(max_length=255, verbose_name=_("Name"), null=True, blank=True) + geom = models.MultiPolygonField(srid=2056, verbose_name=_("Geometry")) + + +@register_oapif_viewset(crs=2056, custom_viewset_attrs={"permission_classes": (permissions.DjangoModelPermissions,)}) +class SecretLayer(BaseModelWithTenFields): + geom = models.PointField(srid=2056, verbose_name=_("Geometry")) diff --git a/src/tests/tests.py b/src/tests/tests.py index 13db2b66..d5fe7107 100644 --- a/src/tests/tests.py +++ b/src/tests/tests.py @@ -36,12 +36,12 @@ def test_post_as_editor(self): "type": "Point", "coordinates": [2508500.0, 1152000.0], }, - "properties": {"field_0": "test123456"}, + "properties": {"field_str_0": "test123456"}, } data_with_crs = data data_with_crs["geometry"]["crs"] = {"type": "name", "properties": {"name": "urn:ogc:def:crs:EPSG::2056"}} - for layer in ("tests.point_2056_10fields_local_json", "tests.point_2056_10fields"): + for layer in ("tests.point_2056_10fields_local_geom", "tests.point_2056_10fields"): for _data in (data, data_with_crs): url = f"{collections_url}/{layer}/items" post_to_items = self.client.post(url, _data, format="json") @@ -50,7 +50,7 @@ def test_post_as_editor(self): def test_anonymous_items_options(self): # Anonymous user expected = {"GET", "OPTIONS", "HEAD"} - for layer in ("tests.point_2056_10fields_local_json", "tests.point_2056_10fields"): + for layer in ("tests.point_2056_10fields_local_geom", "tests.point_2056_10fields"): url = f"{collections_url}/{layer}/items" response = self.client.options(url) @@ -64,7 +64,7 @@ def test_editor_items_options(self): # Authenticated user with editing permissions expected = {"POST", "GET", "OPTIONS", "HEAD"} self.client.force_authenticate(user=self.demo_editor) - for layer in ("tests.point_2056_10fields_local_json", "tests.point_2056_10fields"): + for layer in ("tests.point_2056_10fields_local_geom", "tests.point_2056_10fields"): url = f"{collections_url}/{layer}/items" response = self.client.options(url) diff --git a/tests/benchmark/plot.py b/tests/benchmark/plot.py new file mode 100755 index 00000000..53ea56ae --- /dev/null +++ b/tests/benchmark/plot.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python + +import csv + +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +output_path = "tests/benchmark/results" + + +def tr(layer: str) -> str: + dictionary = { + "point_2056_10fields": "Point, 10 string fields, geometry serialized in DB", + "point_2056_10fields_local_geom": "Point with 12 fields", + "line_2056_10fields": "Line, 10 string fields, geometry serialized in DB", + "line_2056_10fields_local_geom": "Line (3 points) and 12 fields", + "polygon_2056": "Polygon, geometry serialized in DB", + "polygon_2056_local_geom": "Complex polygon (Swiss Municipalities)", + "nogeom_10fields": "No geometry, 12 fields", + "nogeom_100fields": "No geometry, 100 fields", + } + return dictionary[layer] + + +data = {} +with open(f"{output_path}/benchmark.dat") as csvfile: + reader = csv.reader(csvfile, delimiter=",") + for row in reader: + size = int(row[0]) + layer = row[1] + time = float(row[2]) + std = float(row[3]) + if layer not in data: + data[layer] = {} + data[layer][size] = (time, std) + + +def create_fig(title: str = None, showlegend: bool = True) -> go.Figure: + _fig = go.Figure() + configure(_fig, title, showlegend) + return _fig + + +def configure(_fig: go.Figure, title: str = None, showlegend: bool = True): + if title: + fig.update_layout(title_text=title) + _fig.update_layout(margin=dict(l=5, r=5, t=5, b=5)) + _fig.update_layout( + plot_bgcolor="white", + showlegend=showlegend, + autosize=False, + width=600, + height=600, + ) + + _fig.update_xaxes( + mirror=True, + ticks="outside", + showline=True, + linecolor="black", + gridcolor="lightgrey", + tickfont=dict(size=8, color="black"), + showgrid=False, + ) + _fig.update_yaxes( + mirror=True, + ticks="outside", + showline=True, + linecolor="black", + gridcolor="lightgrey", + tickfont=dict(size=8, color="black"), + ) + + +# Time vs Size +plots = {} +for layer, d_ in data.items(): + if "local_geom" not in layer and "nogeom" not in layer: + continue + plots[layer] = ([], [], []) + for size, d__ in d_.items(): + plots[layer][0].append(size) + plots[layer][1].append(d__[0]) + plots[layer][2].append(d__[0] / size) + +fig = create_fig() +fig.update_xaxes(title_text="Number of features", type="log", tickvals=[1, 10, 100, 1000, 10000, 100000]) +fig.update_yaxes(title_text="Fetching time (s)", type="log", tickvals=[0.1, 1, 10, 100, 1000]) +fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)) +for layer, plot in plots.items(): + fig.add_trace(go.Scatter(x=plot[0], y=plot[1], mode="lines+markers", name=tr(layer))) +fig.write_image(f"{output_path}/total_time_vs_size.png", scale=6) + +fig = create_fig() +fig.update_xaxes(title_text="Number of features", type="log") +fig.update_yaxes(title_text="Fetching time per feature (s)") +for layer, plot in plots.items(): + fig.add_trace(go.Scatter(x=plot[0], y=plot[2], mode="lines+markers", name=tr(layer))) +fig.write_image(f"{output_path}/time_per_feature_vs_size.png", scale=6) + + +# local vs non local geom serialization +plots = {} +for layer, d_ in data.items(): + if layer == "secretlayer" or "nogeom" in layer: + continue + + geom = layer.split("_")[0] + if geom not in plots: + plots[geom] = {} + if layer not in plots[geom]: + plots[geom][layer] = ([], [], []) + + for size, d__ in d_.items(): + plots[geom][layer][0].append(size) + plots[geom][layer][1].append(d__[0]) + plots[geom][layer][2].append(d__[1]) + +fig.update_xaxes(title_text="Number of features", type="log") +fig.update_yaxes(title_text="Fetching time (s)") +fig = make_subplots(rows=2, cols=3, row_heights=[0.7, 0.3], shared_xaxes=False, vertical_spacing=0.07) +configure(fig, title="Geometry serialization", showlegend=True) +fig.update_layout(legend=dict(yanchor="top", y=0.94, xanchor="left", x=0.02)) +fig.update_layout(margin=dict(l=5, r=5, t=30, b=5)) +c = 0 +for geom, data in plots.items(): + c += 1 + fig.update_xaxes(type="log", tickmode="array", tickvals=[1, 10, 100, 1000, 10000, 100000], row=1, col=c) + fig.update_yaxes(type="log", tickvals=[0.1, 1, 10, 100], row=1, col=c) + + fig.update_xaxes( + title_text=geom, type="log", tickmode="array", tickvals=[1, 10, 100, 1000, 10000, 100000], row=2, col=c + ) + + colors = ["royalblue", "firebrick"] + names = ["Postgis", "Django"] + + values = list(data.values()) + ratio = [100 * (j - i) / i for i, j in zip(values[0][1], values[1][1])] + + for layer, plot in data.items(): + fig.add_trace(go.Scatter(x=plot[0], y=ratio, showlegend=False, line=dict(color="red")), row=2, col=c) + + fig.add_trace( + go.Scatter( + x=plot[0], + y=plot[1], + error_y=dict(type="data", array=plot[2], visible=False), + mode="lines+markers", + name=names.pop(0), + line=dict(color=colors.pop(0)), + showlegend=(c == 1), + ), + row=1, + col=c, + ) +fig.layout.yaxis4.range = [0, 210] +fig.layout.yaxis5.range = [0, 210] +fig.layout.yaxis6.range = [0, 210] +fig.layout.yaxis1.title.text = "Fetching time per feature (s)" +fig.layout.yaxis4.title.text = "Gain (%)" + +fig.update_layout(legend=dict(traceorder="reversed")) +fig.write_image(f"{output_path}/local_vs_db_geom_serialization.png", scale=6) diff --git a/tests/benchmark/time.sh b/tests/benchmark/time.sh new file mode 100755 index 00000000..864853ab --- /dev/null +++ b/tests/benchmark/time.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -e + +OUTPUT_PATH="tests/benchmark/results" + +mkdir -p ${OUTPUT_PATH} +rm -f ${OUTPUT_PATH}/benchmark.dat + +SIZE=100000 +LAYERS=( point_2056_10fields point_2056_10fields_local_geom nogeom_10fields nogeom_100fields line_2056_10fields line_2056_10fields_local_geom polygon_2056 polygon_2056_local_geom secretlayer ) + + +echo "::group::setup ${SIZE}" +./scripts/populate.sh ${SIZE} +echo "::endgroup::" + +for LAYER in "${LAYERS[@]}"; do + ACTUAL_SIZE=$SIZE + if [[ $LAYER =~ ^polygon_2056.*$ ]]; then + ACTUAL_SIZE=$(( $ACTUAL_SIZE < 2175 ? $ACTUAL_SIZE : 2175 )) + fi + + LIMIT=1 + while [[ $LIMIT -le $SIZE ]]; do + ACTUAL_LIMIT=$(( LIMIT < ACTUAL_SIZE ? LIMIT : ACTUAL_SIZE )) + hyperfine --warmup 2 -r 10 "curl http://${OGCAPIF_HOST}:${DJANGO_DEV_PORT}/oapif/collections/tests.${LAYER}/items?limit=${ACTUAL_LIMIT}" --export-json ${OUTPUT_PATH}/.time.json + echo "$ACTUAL_LIMIT,$LAYER,$(cat ${OUTPUT_PATH}/.time.json | jq -r '.results[0]| [.mean, .stddev] | @csv')" >> ${OUTPUT_PATH}/benchmark.dat + LIMIT=$((LIMIT*10)) + done +done + +rm ${OUTPUT_PATH}/.time.json diff --git a/tests/integration/test_integration_qgis.py b/tests/integration/test_integration_qgis.py index d4fb032d..52741d45 100644 --- a/tests/integration/test_integration_qgis.py +++ b/tests/integration/test_integration_qgis.py @@ -55,14 +55,14 @@ def test_load_layer(self): self.assertIsNotNone(layer) f = None - for f in layer.getFeatures("field_0 is not null"): + for f in layer.getFeatures("field_str_0 is not null"): pass self.assertIsInstance(f, QgsFeature) self.assertFalse(bool(layer.dataProvider().capabilities() & QgsVectorDataProvider.Capability.AddFeatures)) def test_load_and_edit_with_basic_auth(self): - for layer in ("tests.point_2056_10fields_local_json", "tests.point_2056_10fields"): + for layer in ("tests.point_2056_10fields_local_geom", "tests.point_2056_10fields"): uri = QgsDataSourceUri() uri.setParam("service", "wfs") uri.setParam("typename", layer) @@ -80,21 +80,22 @@ def test_load_and_edit_with_basic_auth(self): f = next(layer.getFeatures()) self.assertIsInstance(f, QgsFeature) - # f["field_0"] = "xyz" + # f["field_str_0"] = "xyz" # with edit(layer): # layer.updateFeature(f) - # f = next(layer.getFeatures("field_0='xyz'")) + # f = next(layer.getFeatures("field_str_0='xyz'")) # self.assertIsInstance(f, QgsFeature) # create with geometry f = QgsFeature() f.setFields(layer.fields()) - f["field_0"] = "Super Green" + f["field_bool"] = True + f["field_str_0"] = "Super Green" geom = QgsGeometry.fromPoint(QgsPoint(2345678.0, 1234567.0)) f.setGeometry(geom) with edit(layer): layer.addFeature(f) - f = next(layer.getFeatures("field_0='Super Green'")) + f = next(layer.getFeatures("field_str_0='Super Green'")) self.assertIsInstance(f, QgsFeature) self.assertEqual(geom.asWkt(), f.geometry().asWkt())