diff --git a/.github/workflows/nightly_ci.yml b/.github/workflows/nightly_ci.yml new file mode 100644 index 00000000..e72cd47b --- /dev/null +++ b/.github/workflows/nightly_ci.yml @@ -0,0 +1,53 @@ +name: nightly-ci +on: + schedule: + # Run at 1:14 am every night, to avoid high load at common schedule times. + - cron: "14 1 * * *" + +jobs: + test: + runs-on: ubuntu-22.04 + services: + postgres: + image: postgis/postgis:14-3.3 + env: + POSTGRES_DB: django + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + rabbitmq: + image: rabbitmq:management + ports: + - 5672:5672 + minio: + image: bitnami/minio:latest + env: + MINIO_ROOT_USER: minioAccessKey + MINIO_ROOT_PASSWORD: minioSecretKey + ports: + - 9000:9000 + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install tox + run: | + pip install --upgrade pip + pip install tox + - name: Install GDAL + run: | + sudo apt-add-repository ppa:ubuntugis/ppa + sudo apt-get update + sudo apt-get install gdal-bin libgdal-dev + pip install GDAL==`gdal-config --version` + - name: Run tests + run: | + tox -e test -- -k "slow" + env: + DJANGO_DATABASE_URL: postgres://postgres:postgres@localhost:5432/django + DJANGO_MINIO_STORAGE_ENDPOINT: localhost:9000 + DJANGO_MINIO_STORAGE_ACCESS_KEY: minioAccessKey + DJANGO_MINIO_STORAGE_SECRET_KEY: minioSecretKey + DJANGO_HOMEPAGE_REDIRECT_URL: http://localhost:8080/ diff --git a/dev/.env.docker-compose b/dev/.env.docker-compose index 38c7d82d..30444fbc 100644 --- a/dev/.env.docker-compose +++ b/dev/.env.docker-compose @@ -7,3 +7,4 @@ DJANGO_MINIO_STORAGE_ACCESS_KEY=minioAccessKey DJANGO_MINIO_STORAGE_SECRET_KEY=minioSecretKey DJANGO_STORAGE_BUCKET_NAME=django-storage DJANGO_MINIO_STORAGE_MEDIA_URL=http://localhost:9000/django-storage +DJANGO_HOMEPAGE_REDIRECT_URL=http://localhost:8080/ diff --git a/dev/.env.docker-compose-native b/dev/.env.docker-compose-native index 1b612c14..e022138c 100644 --- a/dev/.env.docker-compose-native +++ b/dev/.env.docker-compose-native @@ -6,3 +6,4 @@ DJANGO_MINIO_STORAGE_ENDPOINT=localhost:9000 DJANGO_MINIO_STORAGE_ACCESS_KEY=minioAccessKey DJANGO_MINIO_STORAGE_SECRET_KEY=minioSecretKey DJANGO_STORAGE_BUCKET_NAME=django-storage +DJANGO_HOMEPAGE_REDIRECT_URL=http://localhost:8080/ diff --git a/setup.py b/setup.py index 691ba224..2cbcd018 100644 --- a/setup.py +++ b/setup.py @@ -70,5 +70,11 @@ 'ipython==8.26.0', 'tox==4.16.0', ], + 'test': [ + 'factory-boy==3.3.1', + 'pytest==8.3.3', + 'pytest-django==4.9.0', + 'pytest-mock==3.14.0', + ], }, ) diff --git a/tox.ini b/tox.ini index 2bb7c68b..9daf0395 100644 --- a/tox.ini +++ b/tox.ini @@ -48,15 +48,10 @@ passenv = DJANGO_HOMEPAGE_REDIRECT_URL extras = dev -deps = - factory-boy - pytest - pytest-django - pytest-factoryboy - pytest-mock + test commands = pip install large-image-converter --find-links https://girder.github.io/large_image_wheels - pytest {posargs} + pytest {posargs:-k "not slow"} [testenv:check-migrations] setenv = @@ -94,6 +89,8 @@ exclude = DJANGO_SETTINGS_MODULE = uvdat.settings DJANGO_CONFIGURATION = TestingConfiguration addopts = --strict-markers --showlocals --verbose +markers = + slow: mark test as slow filterwarnings = # https://github.com/jazzband/django-configurations/issues/190 ignore:the imp module is deprecated in favour of importlib:DeprecationWarning:configurations diff --git a/uvdat/core/models/project.py b/uvdat/core/models/project.py index 7af60d8c..aab151f3 100644 --- a/uvdat/core/models/project.py +++ b/uvdat/core/models/project.py @@ -1,8 +1,10 @@ +import typing + from django.contrib.auth.models import User from django.contrib.gis.db import models as geo_models from django.db import models, transaction from guardian.models import UserObjectPermission -from guardian.shortcuts import assign_perm +from guardian.shortcuts import assign_perm, get_users_with_perms from .dataset import Dataset @@ -13,27 +15,73 @@ class Project(models.Model): default_map_zoom = models.FloatField(default=10) datasets = models.ManyToManyField(Dataset, blank=True) + def owner(self) -> User: + users = typing.cast( + list[User], list(get_users_with_perms(self, only_with_perms_in=['owner'])) + ) + if len(users) != 1: + raise Exception('Project must have exactly one owner') + + return users[0] + + def collaborators(self) -> list[User]: + return typing.cast( + list[User], list(get_users_with_perms(self, only_with_perms_in=['collaborator'])) + ) + + def followers(self): + return typing.cast( + list[User], list(get_users_with_perms(self, only_with_perms_in=['follower'])) + ) + @transaction.atomic() - def set_permissions( - self, - owner: User, - collaborator: list[User] | None = None, - follower: list[User] | None = None, - ): - # Delete all existing first + def delete_users_perms(self, users: list[User]): + """Delete all permissions a user may have on this project.""" + user_ids = [user.id for user in users] + UserObjectPermission.objects.filter( + content_type__app_label=self._meta.app_label, + content_type__model=self._meta.model_name, + object_pk=self.pk, + user_id__in=user_ids, + ).delete() + + @transaction.atomic() + def set_owner(self, user: User): + # Remove existing owner UserObjectPermission.objects.filter( content_type__app_label=self._meta.app_label, content_type__model=self._meta.model_name, object_pk=self.pk, + permission__codename='owner', ).delete() - # Assign new perms - assign_perm('owner', owner, self) - for user in collaborator or []: + # Delete any existing user perms and set owner + self.delete_users_perms([user]) + assign_perm('owner', user, self) + + @transaction.atomic() + def add_collaborators(self, users: list[User]): + self.delete_users_perms(users) + for user in users: assign_perm('collaborator', user, self) - for user in follower or []: + + @transaction.atomic() + def add_followers(self, users: list[User]): + self.delete_users_perms(users) + for user in users: assign_perm('follower', user, self) + @transaction.atomic() + def set_permissions( + self, + owner: User, + collaborator: list[User] | None = None, + follower: list[User] | None = None, + ): + self.set_owner(owner) + self.add_collaborators(collaborator or []) + self.add_followers(follower or []) + class Meta: permissions = [ ('owner', 'Can read, write, and delete'), diff --git a/uvdat/core/rest/__init__.py b/uvdat/core/rest/__init__.py index 26d32d21..4fbf6b9c 100644 --- a/uvdat/core/rest/__init__.py +++ b/uvdat/core/rest/__init__.py @@ -1,8 +1,6 @@ from .chart import ChartViewSet from .dataset import DatasetViewSet -from .file_item import FileItemViewSet from .map_layers import RasterMapLayerViewSet, VectorMapLayerViewSet -from .network import NetworkEdgeViewSet, NetworkNodeViewSet, NetworkViewSet from .project import ProjectViewSet from .regions import SourceRegionViewSet from .simulations import SimulationViewSet @@ -11,12 +9,8 @@ __all__ = [ ProjectViewSet, ChartViewSet, - FileItemViewSet, RasterMapLayerViewSet, VectorMapLayerViewSet, - NetworkViewSet, - NetworkNodeViewSet, - NetworkEdgeViewSet, DatasetViewSet, SourceRegionViewSet, SimulationViewSet, diff --git a/uvdat/core/rest/access_control.py b/uvdat/core/rest/access_control.py index f8293004..980b2b41 100644 --- a/uvdat/core/rest/access_control.py +++ b/uvdat/core/rest/access_control.py @@ -8,12 +8,16 @@ from uvdat.core.models.project import Project -# TODO: Dataset permissions should be separated from Project permissions def filter_queryset_by_projects(queryset: QuerySet[Model], projects: QuerySet[models.Project]): model = queryset.model + + # Dataset permissions not yet implemented, and as such, all datasets are visible to all users + if model == models.Dataset: + return queryset + if model == models.Project: return queryset.filter(id__in=projects.values_list('id', flat=True)) - if model in [models.Dataset, models.Chart, models.SimulationResult]: + if model in [models.Chart, models.SimulationResult]: return queryset.filter(project__in=projects) if model in [ models.FileItem, diff --git a/uvdat/core/rest/dataset.py b/uvdat/core/rest/dataset.py index dbfed9f7..dabcdf29 100644 --- a/uvdat/core/rest/dataset.py +++ b/uvdat/core/rest/dataset.py @@ -1,9 +1,8 @@ -import json - -from django.http import HttpResponse +from drf_yasg.utils import swagger_auto_schema +from rest_framework import serializers from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import ReadOnlyModelViewSet from uvdat.core.models import Dataset, Network, NetworkEdge, NetworkNode from uvdat.core.rest.access_control import GuardianFilter, GuardianPermission @@ -17,7 +16,12 @@ from uvdat.core.tasks.chart import add_gcc_chart_datum -class DatasetViewSet(ModelViewSet): +class GCCQueryParamSerializer(serializers.Serializer): + project = serializers.IntegerField() + exclude_nodes = serializers.RegexField(r'^\d+(,\s?\d+)*$') + + +class DatasetViewSet(ReadOnlyModelViewSet): queryset = Dataset.objects.all() serializer_class = DatasetSerializer permission_classes = [GuardianPermission] @@ -49,12 +53,6 @@ def map_layers(self, request, **kwargs): # Return response with rendered data return Response(serializer.data, status=200) - @action(detail=True, methods=['get']) - def convert(self, request, **kwargs): - dataset = self.get_object() - dataset.spawn_conversion_task() - return HttpResponse(status=200) - @action(detail=True, methods=['get']) def network(self, request, **kwargs): dataset = self.get_object() @@ -72,15 +70,21 @@ def network(self, request, **kwargs): ], } ) - return HttpResponse(json.dumps(networks), status=200) + return Response(networks, status=200) + @swagger_auto_schema(query_serializer=GCCQueryParamSerializer) @action(detail=True, methods=['get']) def gcc(self, request, **kwargs): dataset = self.get_object() - project_id = request.query_params.get('project') - exclude_nodes = request.query_params.get('exclude_nodes', []) - exclude_nodes = exclude_nodes.split(',') - exclude_nodes = [int(n) for n in exclude_nodes if len(n)] + + # Validate and de-serialize query params + serializer = GCCQueryParamSerializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + project_id = serializer.validated_data['project'] + exclude_nodes = [int(n) for n in serializer.validated_data['exclude_nodes'].split(',')] + + if not dataset.networks.exists(): + return Response(data='No networks exist in selected dataset', status=400) # Find the GCC for each network in the dataset network_gccs: list[list[int]] = [] diff --git a/uvdat/core/rest/file_item.py b/uvdat/core/rest/file_item.py deleted file mode 100644 index 51a2054e..00000000 --- a/uvdat/core/rest/file_item.py +++ /dev/null @@ -1,13 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from uvdat.core.models import FileItem -from uvdat.core.rest.access_control import GuardianFilter, GuardianPermission -from uvdat.core.rest.serializers import FileItemSerializer - - -class FileItemViewSet(ModelViewSet): - queryset = FileItem.objects.all() - serializer_class = FileItemSerializer - permission_classes = [GuardianPermission] - filter_backends = [GuardianFilter] - lookup_field = 'id' diff --git a/uvdat/core/rest/map_layers.py b/uvdat/core/rest/map_layers.py index 39a53ce0..2a360883 100644 --- a/uvdat/core/rest/map_layers.py +++ b/uvdat/core/rest/map_layers.py @@ -3,9 +3,10 @@ from django.db import connection from django.http import HttpResponse from django_large_image.rest import LargeImageFileDetailMixin +from rest_framework import mixins from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import GenericViewSet from uvdat.core.models import RasterMapLayer, VectorMapLayer from uvdat.core.rest.access_control import GuardianFilter, GuardianPermission @@ -70,7 +71,7 @@ """ -class RasterMapLayerViewSet(ModelViewSet, LargeImageFileDetailMixin): +class RasterMapLayerViewSet(GenericViewSet, mixins.RetrieveModelMixin, LargeImageFileDetailMixin): queryset = RasterMapLayer.objects.select_related('dataset').all() serializer_class = RasterMapLayerSerializer permission_classes = [GuardianPermission] @@ -90,7 +91,7 @@ def get_raster_data(self, request, resolution: str = '1', **kwargs): return HttpResponse(json.dumps(data), status=200) -class VectorMapLayerViewSet(ModelViewSet): +class VectorMapLayerViewSet(GenericViewSet, mixins.RetrieveModelMixin): queryset = VectorMapLayer.objects.select_related('dataset').all() serializer_class = VectorMapLayerSerializer permission_classes = [GuardianPermission] diff --git a/uvdat/core/rest/network.py b/uvdat/core/rest/network.py deleted file mode 100644 index a3ca45dc..00000000 --- a/uvdat/core/rest/network.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from uvdat.core.models import Network, NetworkEdge, NetworkNode -from uvdat.core.rest.access_control import GuardianFilter, GuardianPermission -from uvdat.core.rest.serializers import ( - NetworkEdgeSerializer, - NetworkNodeSerializer, - NetworkSerializer, -) - - -class NetworkViewSet(ModelViewSet): - queryset = Network.objects.all() - serializer_class = NetworkSerializer - permission_classes = [GuardianPermission] - filter_backends = [GuardianFilter] - lookup_field = 'id' - - -class NetworkNodeViewSet(ModelViewSet): - queryset = NetworkNode.objects.all() - serializer_class = NetworkNodeSerializer - permission_classes = [GuardianPermission] - filter_backends = [GuardianFilter] - lookup_field = 'id' - - -class NetworkEdgeViewSet(ModelViewSet): - queryset = NetworkEdge.objects.all() - serializer_class = NetworkEdgeSerializer - permission_classes = [GuardianPermission] - filter_backends = [GuardianFilter] - lookup_field = 'id' diff --git a/uvdat/core/rest/project.py b/uvdat/core/rest/project.py index 8ddb1558..f202fe82 100644 --- a/uvdat/core/rest/project.py +++ b/uvdat/core/rest/project.py @@ -1,3 +1,4 @@ +import typing from typing import Any from django.contrib.auth.models import User @@ -29,6 +30,15 @@ def perform_create(self, serializer): @swagger_auto_schema(method='PUT', request_body=ProjectPermissionsSerializer) @action(detail=True, methods=['PUT']) def permissions(self, request: Request, *args: Any, **kwargs: Any): + if request.user.is_anonymous: + raise Exception('Anonymous user received after guardian filter') + user = typing.cast(User, request.user) + + # Only the owner can modify project permissions + project: Project = self.get_object() + if not user.has_perm('owner', project): # type: ignore + return Response(status=403) + serializer = ProjectPermissionsSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -48,6 +58,7 @@ def simulation_results(self, request, **kwargs): simulation_results = project.simulation_results.all() return HttpResponse(simulation_results, status=200) + # TODO: This should be a POST @action( detail=True, methods=['get'], diff --git a/uvdat/core/rest/serializers.py b/uvdat/core/rest/serializers.py index be556bd6..de30f7cd 100644 --- a/uvdat/core/rest/serializers.py +++ b/uvdat/core/rest/serializers.py @@ -1,7 +1,6 @@ from django.contrib.auth.models import User from django.contrib.gis.geos import Point from django.contrib.gis.serializers import geojson -from guardian.shortcuts import get_users_with_perms from rest_framework import serializers from uvdat.core.models import ( @@ -55,20 +54,14 @@ def get_center(self, obj): if obj.default_map_center: return [obj.default_map_center.y, obj.default_map_center.x] - def get_owner(self, obj): - users = list(get_users_with_perms(obj, only_with_perms_in=['owner'])) - if len(users) != 1: - raise Exception('Project must have exactly one owner') + def get_owner(self, obj: Project): + return UserSerializer(obj.owner()).data - return UserSerializer(users[0]).data + def get_collaborators(self, obj: Project): + return [UserSerializer(user).data for user in obj.collaborators()] - def get_collaborators(self, obj): - users = get_users_with_perms(obj, only_with_perms_in=['collaborator']) - return [UserSerializer(user).data for user in users.all()] - - def get_followers(self, obj): - users = get_users_with_perms(obj, only_with_perms_in=['follower']) - return [UserSerializer(user).data for user in users.all()] + def get_followers(self, obj: Project): + return [UserSerializer(user).data for user in obj.followers()] def get_item_counts(self, obj): return { diff --git a/uvdat/core/rest/simulations.py b/uvdat/core/rest/simulations.py index 643da989..18c08c9e 100644 --- a/uvdat/core/rest/simulations.py +++ b/uvdat/core/rest/simulations.py @@ -5,7 +5,7 @@ from django.http import HttpResponse from rest_framework.decorators import action from rest_framework.serializers import ModelSerializer -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import GenericViewSet from uvdat.core.models import Project from uvdat.core.models.simulations import AVAILABLE_SIMULATIONS, SimulationResult @@ -68,7 +68,7 @@ def get_available_simulations(project_id: int): return sims -class SimulationViewSet(ModelViewSet): +class SimulationViewSet(GenericViewSet): queryset = SimulationResult.objects.all() serializer_class = uvdat_serializers.SimulationResultSerializer permission_classes = [GuardianPermission] diff --git a/uvdat/core/tests/conftest.py b/uvdat/core/tests/conftest.py index 8d92203d..751a343b 100644 --- a/uvdat/core/tests/conftest.py +++ b/uvdat/core/tests/conftest.py @@ -1,12 +1,29 @@ from django.contrib.auth.models import User from django.contrib.gis.geos import Point import pytest -from pytest_factoryboy import register from rest_framework.test import APIClient from uvdat.core.models import Project -from .factories import UserFactory +from .factory_fixtures import * # noqa: F403, F401 + + +@pytest.fixture +def project_owner(project: Project) -> User: + return project.owner() + + +@pytest.fixture +def project_collaborator(user, project: Project) -> User: + project.add_collaborators([user]) + return user + + +@pytest.fixture +def project_follower(user, project: Project) -> User: + project.add_followers([user]) + return user + USER_INFOS = [ dict( @@ -67,6 +84,13 @@ def api_client() -> APIClient: return APIClient() +@pytest.fixture +def authenticated_api_client(user) -> APIClient: + client = APIClient() + client.force_authenticate(user=user) + return client + + @pytest.fixture def permissions_client(user_info, test_project) -> APIClient: user_info.pop('perm', None) @@ -75,6 +99,3 @@ def permissions_client(user_info, test_project) -> APIClient: client = APIClient() client.force_authenticate(user=user) return (client, user) - - -register(UserFactory) diff --git a/uvdat/core/tests/data/sample_cog.tif b/uvdat/core/tests/data/sample_cog.tif new file mode 100644 index 00000000..2c7eb08d Binary files /dev/null and b/uvdat/core/tests/data/sample_cog.tif differ diff --git a/uvdat/core/tests/data/sample_geo.json b/uvdat/core/tests/data/sample_geo.json new file mode 100644 index 00000000..c009cedc --- /dev/null +++ b/uvdat/core/tests/data/sample_geo.json @@ -0,0 +1,82 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 102, + 0.5 + ] + }, + "properties": { + "prop0": "value0" + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 102, + 0 + ], + [ + 103, + 1 + ], + [ + 104, + 0 + ], + [ + 105, + 1 + ] + ] + }, + "properties": { + "prop0": "value0", + "prop1": 0 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 100, + 0 + ], + [ + 101, + 0 + ], + [ + 101, + 1 + ], + [ + 100, + 1 + ], + [ + 100, + 0 + ] + ] + ] + }, + "properties": { + "prop0": "value0", + "prop1": { + "this": "that" + } + } + } + ] +} \ No newline at end of file diff --git a/uvdat/core/tests/factories.py b/uvdat/core/tests/factories.py index 44213132..e0165e6c 100644 --- a/uvdat/core/tests/factories.py +++ b/uvdat/core/tests/factories.py @@ -1,5 +1,25 @@ +from pathlib import Path + from django.contrib.auth.models import User +from django.contrib.gis.geos import LineString, Point import factory.django +from factory.faker import faker +import factory.fuzzy + +from uvdat.core.models import Dataset, Project +from uvdat.core.models.map_layers import RasterMapLayer, VectorMapLayer +from uvdat.core.models.networks import Network, NetworkEdge, NetworkNode + + +class FuzzyPointField(factory.fuzzy.BaseFuzzyAttribute): + """Yield random point.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def fuzz(self): + fake = faker.Faker() + return Point((fake.latitude(), fake.longitude())) class UserFactory(factory.django.DjangoModelFactory): @@ -10,3 +30,97 @@ class Meta: email = factory.Faker('safe_email') first_name = factory.Faker('first_name') last_name = factory.Faker('last_name') + + +class SuperUserFactory(UserFactory): + class Meta: + model = User + + username = factory.SelfAttribute('email') + email = factory.Faker('safe_email') + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name') + is_superuser = True + + +class ProjectFactory(factory.django.DjangoModelFactory): + class Meta: + model = Project + + name = factory.Faker('name') + default_map_center = FuzzyPointField() + default_map_zoom = factory.Faker('pyfloat', min_value=0, max_value=22) + + +class DatasetFactory(factory.django.DjangoModelFactory): + class Meta: + model = Dataset + + name = factory.Faker('name') + dataset_type = Dataset.DatasetType.VECTOR + category = factory.Faker( + 'random_element', + elements=[ + 'climate', + 'elevation', + 'region', + 'flood', + 'transportation', + 'energy', + ], + ) + + +class NetworkFactory(factory.django.DjangoModelFactory): + class Meta: + model = Network + + dataset = factory.SubFactory(DatasetFactory) + + +class NetworkNodeFactory(factory.django.DjangoModelFactory): + class Meta: + model = NetworkNode + + name = factory.Faker('name') + network = factory.SubFactory(NetworkFactory) + location = FuzzyPointField() + + +class NetworkEdgeFactory(factory.django.DjangoModelFactory): + class Meta: + model = NetworkEdge + + name = factory.Faker('name') + network = factory.SubFactory(NetworkFactory) + + # TODO: Fix bug where both of these fields point to the same node + # Ensure the edge and both nodes are in the same network + from_node = factory.SubFactory(NetworkNodeFactory, network=factory.SelfAttribute('..network')) + to_node = factory.SubFactory(NetworkNodeFactory, network=factory.SelfAttribute('..network')) + + @factory.lazy_attribute + def line_geometry(self): + return LineString(self.from_node.location, self.to_node.location) + + +class RasterMapLayerFactory(factory.django.DjangoModelFactory): + class Meta: + model = RasterMapLayer + + dataset = factory.SubFactory(DatasetFactory) + cloud_optimized_geotiff = factory.django.FileField( + filename=factory.Faker('file_name', extension='tif'), + from_path=Path(__file__).parent / 'data' / 'sample_cog.tif', + ) + + +class VectorMapLayerFactory(factory.django.DjangoModelFactory): + class Meta: + model = VectorMapLayer + + dataset = factory.SubFactory(DatasetFactory) + geojson_file = factory.django.FileField( + filename=factory.Faker('file_name', extension='json'), + from_path=Path(__file__).parent / 'data' / 'sample_geo.json', + ) diff --git a/uvdat/core/tests/factory_fixtures.py b/uvdat/core/tests/factory_fixtures.py new file mode 100644 index 00000000..2b2f0d32 --- /dev/null +++ b/uvdat/core/tests/factory_fixtures.py @@ -0,0 +1,116 @@ +import pytest + +from uvdat.core.models import Project + +from .factories import ( + DatasetFactory, + NetworkEdgeFactory, + NetworkFactory, + NetworkNodeFactory, + ProjectFactory, + RasterMapLayerFactory, + SuperUserFactory, + UserFactory, + VectorMapLayerFactory, +) + + +# User +@pytest.fixture +def user_factory(): + return UserFactory + + +@pytest.fixture +def user(user_factory): + return user_factory() + + +@pytest.fixture +def superuser_factory(): + return SuperUserFactory + + +@pytest.fixture +def superuser(superuser_factory): + return superuser_factory() + + +# Project +@pytest.fixture +def project_factory(): + return ProjectFactory + + +# Ensure that when a project is created, it always has an owner +@pytest.fixture +def project(user_factory, project_factory) -> Project: + project = project_factory() + project.set_owner(user_factory()) + return project + + +# Dataset +@pytest.fixture +def dataset_factory(): + return DatasetFactory + + +@pytest.fixture +def dataset(dataset_factory): + return dataset_factory() + + +# Network +@pytest.fixture +def network_factory(): + return NetworkFactory + + +@pytest.fixture +def network(network_factory): + return network_factory() + + +# Network Node +@pytest.fixture +def network_node_factory(): + return NetworkNodeFactory + + +@pytest.fixture +def network_node(network_node_factory): + return network_node_factory() + + +# Network Edge +@pytest.fixture +def network_edge_factory(): + return NetworkEdgeFactory + + +@pytest.fixture +def network_edge(network_edge_factory): + return network_edge_factory() + + +# Raster Map Layer +@pytest.fixture +def raster_map_layer_factory(): + return RasterMapLayerFactory + + +@pytest.fixture +def raster_map_layer(raster_map_layer_factory): + return raster_map_layer_factory() + + +# Vector Map Layer +@pytest.fixture +def vector_map_layer_factory(): + return VectorMapLayerFactory + + +@pytest.fixture +def vector_map_layer(vector_map_layer_factory): + return vector_map_layer_factory() diff --git a/uvdat/core/tests/test_api.py b/uvdat/core/tests/test_api.py deleted file mode 100644 index 7218f0f9..00000000 --- a/uvdat/core/tests/test_api.py +++ /dev/null @@ -1,317 +0,0 @@ -from django.contrib.gis.geos import LineString, MultiPolygon, Point, Polygon -from guardian.shortcuts import get_perms -import pytest - -from uvdat.core.models import ( - Chart, - Dataset, - FileItem, - Network, - NetworkEdge, - NetworkNode, - RasterMapLayer, - SourceRegion, - VectorMapLayer, -) - -from .conftest import USER_INFOS - - -def list_endpoint(server, client, viewset_name, **kwargs): - read_allowed = kwargs.get('read_allowed', False) - api_root = f'{server.url}/api/v1' - response = client.get(f'{api_root}/{viewset_name}/', format='json') - assert response.status_code == 200 - if not read_allowed: - assert response.json().get('count') == 0 - else: - assert response.json().get('count') == 1 - - -def fetch_endpoint(server, client, viewset_name, obj_id, **kwargs): - read_allowed = kwargs.get('read_allowed', False) - api_root = f'{server.url}/api/v1' - response = client.get(f'{api_root}/{viewset_name}/{obj_id}/', format='json') - if not read_allowed: - assert response.status_code == 404 - else: - assert response.status_code == 200 - assert response.json().get('id') == obj_id - - -def create_endpoint(server, client, viewset_name, post_data, **kwargs): - if post_data is not None: - api_root = f'{server.url}/api/v1' - response = client.post(f'{api_root}/{viewset_name}/', post_data, format='json') - assert response.status_code == 201 - for key, value in post_data.items(): - assert response.json().get(key) == value - - -def overwrite_endpoint(server, client, viewset_name, obj_id, put_data, **kwargs): - if put_data is not None: - read_allowed = kwargs.get('read_allowed', False) - write_allowed = kwargs.get('write_allowed', False) - api_root = f'{server.url}/api/v1' - response = client.put(f'{api_root}/{viewset_name}/{obj_id}/', put_data, format='json') - if not read_allowed: - assert response.status_code == 404 - elif not write_allowed: - assert response.status_code == 403 - else: - assert response.status_code == 200 - for key, value in put_data.items(): - assert response.json().get(key) == value - - -def update_endpoint(server, client, viewset_name, obj_id, patch_data, **kwargs): - if patch_data is not None: - read_allowed = kwargs.get('read_allowed', False) - write_allowed = kwargs.get('write_allowed', False) - api_root = f'{server.url}/api/v1' - response = client.patch(f'{api_root}/{viewset_name}/{obj_id}/', patch_data, format='json') - if not read_allowed: - assert response.status_code == 404 - elif not write_allowed: - assert response.status_code == 403 - else: - assert response.status_code == 200 - for key, value in patch_data.items(): - assert response.json().get(key) == value - - -def delete_endpoint(server, client, viewset_name, obj_id, **kwargs): - perform_delete = kwargs.get('perform_delete', True) - read_allowed = kwargs.get('read_allowed', False) - delete_allowed = kwargs.get('write_allowed', False) - api_root = f'{server.url}/api/v1' - if perform_delete: - response = client.delete(f'{api_root}/{viewset_name}/{obj_id}/', format='json') - if not read_allowed: - assert response.status_code == 404 - elif not delete_allowed: - assert response.status_code == 403 - else: - assert response.status_code == 204 - - -def viewset_test( - live_server, - permissions_client, - user_info, - test_project, - viewset_name, - obj, - post_data=None, - put_data=None, - patch_data=None, - perform_delete=True, -): - client, user = permissions_client - perm = user_info.get('perm') - superuser = user_info.get('is_superuser') - args = [live_server, client, viewset_name] - kwargs = dict( - read_allowed=perm is not None or superuser, - write_allowed=perm in ['collaborator', 'owner'] or superuser, - delete_allowed=perm == 'owner' or superuser, - perform_delete=perform_delete, - ) - - # Update project permissions - if perm is not None: - test_project.set_permissions(**{perm: user if perm == 'owner' else [user]}) - assert get_perms(user, test_project) == [perm] - - # Test endpoints - list_endpoint(*args, **kwargs) - fetch_endpoint(*args, obj.id, **kwargs) - create_endpoint(*args, post_data, **kwargs) - overwrite_endpoint(*args, obj.id, put_data, **kwargs) - update_endpoint(*args, obj.id, patch_data, **kwargs) - delete_endpoint(*args, obj.id, **kwargs) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_project_viewset(live_server, permissions_client, user_info, test_project): - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'projects', - test_project, - post_data=dict(name='New Project', default_map_center=[42, -71], default_map_zoom=10), - put_data=dict( - name='Overwritten Test Project', default_map_center=[42, -71], default_map_zoom=10 - ), - patch_data=dict(name='Updated Test Project'), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_chart_viewset(live_server, permissions_client, user_info, test_project): - chart = Chart.objects.create(name='Test Chart', project=test_project) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'charts', - chart, - post_data=dict(name='New Chart'), - put_data=dict(name='Overwritten Test Chart'), - patch_data=dict(name='Updated Test Chart'), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_dataset_viewset(live_server, permissions_client, user_info, test_project): - dataset = Dataset.objects.create(name='Test Dataset') - test_project.datasets.add(dataset) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'datasets', - dataset, - post_data=dict(name='New Dataset', dataset_type='VECTOR', category='test'), - put_data=dict(name='Overwritten Test Dataset', dataset_type='VECTOR', category='test'), - patch_data=dict(name='Updated Test Dataset'), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_files_viewset(live_server, permissions_client, user_info, test_project): - dataset = Dataset.objects.create(name='Test Dataset') - test_project.datasets.add(dataset) - file_item = FileItem.objects.create(name='Test File', dataset=dataset) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'files', - file_item, - patch_data=dict(name='Updated Test File'), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_raster_viewset(live_server, permissions_client, user_info, test_project): - dataset = Dataset.objects.create(name='Test Dataset') - test_project.datasets.add(dataset) - raster = RasterMapLayer.objects.create(dataset=dataset) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'rasters', - raster, - patch_data=dict(index=1), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_vector_viewset(live_server, permissions_client, user_info, test_project): - dataset = Dataset.objects.create(name='Test Dataset') - test_project.datasets.add(dataset) - vector = VectorMapLayer.objects.create(dataset=dataset) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'vectors', - vector, - patch_data=dict(index=1), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_network_viewset(live_server, permissions_client, user_info, test_project): - dataset = Dataset.objects.create(name='Test Dataset') - test_project.datasets.add(dataset) - network = Network.objects.create(dataset=dataset) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'networks', - network, - patch_data=dict(category='foo'), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_node_viewset(live_server, permissions_client, user_info, test_project): - dataset = Dataset.objects.create(name='Test Dataset') - test_project.datasets.add(dataset) - network = Network.objects.create(dataset=dataset) - node = NetworkNode.objects.create(name='Test Node', network=network, location=Point(42, -71)) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'nodes', - node, - patch_data=dict(name='Updated Test Node'), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_edge_viewset(live_server, permissions_client, user_info, test_project): - dataset = Dataset.objects.create(name='Test Dataset') - test_project.datasets.add(dataset) - network = Network.objects.create(dataset=dataset) - node_1 = NetworkNode.objects.create(name='Test Node', network=network, location=Point(42, -71)) - node_2 = NetworkNode.objects.create(name='Test Node', network=network, location=Point(41, -70)) - edge = NetworkEdge.objects.create( - name='Test Edge', - network=network, - line_geometry=LineString(Point(42, -71), Point(41, -70)), - from_node=node_1, - to_node=node_2, - ) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'edges', - edge, - patch_data=dict(name='Updated Test Edge'), - ) - - -@pytest.mark.parametrize('user_info', USER_INFOS, ids=[u.get('id') for u in USER_INFOS]) -@pytest.mark.django_db -def test_region_viewset(live_server, permissions_client, user_info, test_project): - dataset = Dataset.objects.create(name='Test Dataset') - test_project.datasets.add(dataset) - geo_points = (Point(42, -71), Point(41, -70), Point(41.5, -70.5), Point(42, -71)) - region = SourceRegion.objects.create( - name='Test Region', dataset=dataset, boundary=MultiPolygon(Polygon(geo_points)) - ) - viewset_test( - live_server, - permissions_client, - user_info, - test_project, - 'source-regions', - region, - perform_delete=False, - ) diff --git a/uvdat/core/tests/test_dataset.py b/uvdat/core/tests/test_dataset.py new file mode 100644 index 00000000..67552814 --- /dev/null +++ b/uvdat/core/tests/test_dataset.py @@ -0,0 +1,168 @@ +import itertools + +import pytest + +from uvdat.core.models.networks import Network, NetworkNode +from uvdat.core.models.project import Dataset, Project + + +@pytest.mark.django_db +def test_rest_dataset_list_retrieve_unauthenticated(api_client): + resp = api_client.get('/api/v1/datasets/') + assert resp.status_code == 401 + + +@pytest.mark.django_db +def test_rest_dataset_list_retrieve(authenticated_api_client, dataset: Dataset): + resp = authenticated_api_client.get('/api/v1/datasets/') + assert len(resp.json()['results']) == 1 + assert resp.json()['results'][0]['id'] == dataset.id + + resp = authenticated_api_client.get(f'/api/v1/datasets/{dataset.id}/') + assert resp.json()['id'] == dataset.id + + +@pytest.mark.django_db +def test_rest_dataset_gcc_no_networks(authenticated_api_client, dataset: Dataset, project: Project): + project.datasets.add(dataset) + resp = authenticated_api_client.get( + f'/api/v1/datasets/{dataset.id}/gcc/?project={project.id}&exclude_nodes=1' + ) + assert resp.status_code == 400 + + +@pytest.mark.django_db +def test_rest_dataset_gcc_empty_network( + authenticated_api_client, project: Project, network: Network +): + dataset = network.dataset + project.datasets.add(dataset) + resp = authenticated_api_client.get( + f'/api/v1/datasets/{dataset.id}/gcc/?project={project.id}&exclude_nodes=1' + ) + + assert resp.status_code == 200 + assert resp.json() == [] + + +@pytest.mark.parametrize('group_sizes', [(3, 2), (20, 3)]) +@pytest.mark.django_db +def test_rest_dataset_gcc( + authenticated_api_client, + project: Project, + network: Network, + network_edge_factory, + network_node_factory, + group_sizes, +): + group_a_size, group_b_size = group_sizes + + # Create two groups of nodes that fully connected + group_a = [network_node_factory(network=network) for _ in range(group_a_size)] + for from_node, to_node in itertools.combinations(group_a, 2): + network_edge_factory(network=network, from_node=from_node, to_node=to_node) + + group_b = [network_node_factory(network=network) for _ in range(group_b_size)] + for from_node, to_node in itertools.combinations(group_b, 2): + network_edge_factory(network=network, from_node=from_node, to_node=to_node) + + # Join these two groups by a single node + connecting_node: NetworkNode = network_node_factory(network=network) + network_edge_factory(network=network, from_node=group_a[0], to_node=connecting_node) + network_edge_factory(network=network, from_node=group_b[0], to_node=connecting_node) + + # Network should look like this + # * * + # | | + # * ---- * ---- * + # | + # * + + dataset = network.dataset + project.datasets.add(dataset) + resp = authenticated_api_client.get( + f'/api/v1/datasets/{dataset.id}/gcc/' + f'?project={project.id}&exclude_nodes={connecting_node.id}' + ) + + larger_group: list[NetworkNode] = max(group_a, group_b, key=len) + assert resp.status_code == 200 + assert sorted(resp.json()) == sorted([n.id for n in larger_group]) + + +@pytest.mark.parametrize('dataset_type', [x[0] for x in Dataset.DatasetType.choices]) +@pytest.mark.django_db +def test_rest_dataset_map_layers_incorrect_layer_type( + authenticated_api_client, + dataset_factory, + raster_map_layer_factory, + vector_map_layer_factory, + dataset_type, +): + dataset = dataset_factory(dataset_type=dataset_type) + + # Intentionally create the wrong map layer type + factory = ( + vector_map_layer_factory + if dataset_type == Dataset.DatasetType.RASTER + else raster_map_layer_factory + ) + for _ in range(3): + factory(dataset=dataset) + + # Check that nothing is returned, since map_layers will only + # return map layers that match the dataset's type + resp = authenticated_api_client.get(f'/api/v1/datasets/{dataset.id}/map_layers/') + assert resp.status_code == 200 + assert resp.json() == [] + + +@pytest.mark.parametrize('dataset_type', [x[0] for x in Dataset.DatasetType.choices]) +@pytest.mark.django_db +def test_rest_dataset_map_layers( + authenticated_api_client, + dataset_factory, + raster_map_layer_factory, + vector_map_layer_factory, + dataset_type, +): + dataset = dataset_factory(dataset_type=dataset_type) + factory = ( + vector_map_layer_factory + if dataset_type == Dataset.DatasetType.VECTOR + else raster_map_layer_factory + ) + map_layers = [factory(dataset=dataset) for _ in range(3)] + + resp = authenticated_api_client.get(f'/api/v1/datasets/{dataset.id}/map_layers/') + assert resp.status_code == 200 + + data: list[dict] = resp.json() + assert len(data) == 3 + + # Assert these lists are the same objects + assert sorted([x['id'] for x in data]) == sorted([x.id for x in map_layers]) + + +@pytest.mark.django_db +def test_rest_dataset_network_no_network(authenticated_api_client, dataset: Dataset): + resp = authenticated_api_client.get(f'/api/v1/datasets/{dataset.id}/network/') + assert resp.status_code == 200 + assert not resp.json() + + +@pytest.mark.django_db +def test_rest_dataset_network(authenticated_api_client, network_edge): + network = network_edge.network + dataset = network.dataset + assert network_edge.from_node != network_edge.to_node + + resp = authenticated_api_client.get(f'/api/v1/datasets/{dataset.id}/network/') + assert resp.status_code == 200 + + data: list[dict] = resp.json() + assert len(data) == 1 + + data: dict = data[0] + assert len(data['nodes']) == 2 + assert len(data['edges']) == 1 diff --git a/uvdat/core/tests/test_load_roads.py b/uvdat/core/tests/test_load_roads.py index 0a1b9264..a954d928 100644 --- a/uvdat/core/tests/test_load_roads.py +++ b/uvdat/core/tests/test_load_roads.py @@ -5,6 +5,7 @@ from uvdat.core.models import Dataset, Project +@pytest.mark.slow @pytest.mark.django_db def test_load_roads(): project = Project.objects.create( diff --git a/uvdat/core/tests/test_populate.py b/uvdat/core/tests/test_populate.py index ec221473..d58807ba 100644 --- a/uvdat/core/tests/test_populate.py +++ b/uvdat/core/tests/test_populate.py @@ -18,6 +18,7 @@ ) +@pytest.mark.slow @pytest.mark.django_db def test_populate(): # ensure a superuser exists diff --git a/uvdat/core/tests/test_project.py b/uvdat/core/tests/test_project.py new file mode 100644 index 00000000..65e5797d --- /dev/null +++ b/uvdat/core/tests/test_project.py @@ -0,0 +1,169 @@ +import faker +import pytest + +from uvdat.core.models.project import Project + + +@pytest.mark.django_db +def test_project_set_owner(project, user): + owner = project.owner() + assert owner.id != user.id + + project.set_owner(user) + assert project.owner().id == user.id + + +@pytest.mark.django_db +def test_project_add_followers_collaborators(project, user_factory): + def sort_func(user): + return user.id + + users = sorted([user_factory() for _ in range(5)], key=sort_func) + assert not project.followers() + assert not project.collaborators() + + project.add_followers(users) + + # Check that users added as collaborators were automatically removed from followers + project.add_collaborators(users) + assert not project.followers() + assert sorted(project.collaborators(), key=sort_func) == users + + # Check that users added as followers were automatically removed from collaborators + project.add_followers(users) + assert not project.collaborators() + assert sorted(project.followers(), key=sort_func) == users + + +@pytest.mark.django_db +def test_rest_project_create_no_datasets(authenticated_api_client): + fake = faker.Faker() + resp = authenticated_api_client.post( + '/api/v1/projects/', + data={ + 'name': fake.name(), + 'default_map_zoom': fake.pyfloat(min_value=0, max_value=22), + 'default_map_center': [fake.latitude(), fake.longitude()], + }, + ) + + assert resp.status_code == 201 + + +@pytest.mark.django_db +def test_rest_project_create_with_datasets(authenticated_api_client, dataset_factory): + fake = faker.Faker() + + datasets = [dataset_factory().id for _ in range(3)] + resp = authenticated_api_client.post( + '/api/v1/projects/', + data={ + 'name': fake.name(), + 'default_map_zoom': fake.pyfloat(min_value=0, max_value=22), + 'default_map_center': [fake.latitude(), fake.longitude()], + 'datasets': datasets, + }, + ) + + assert resp.status_code == 201 + + +@pytest.mark.django_db +def test_rest_project_retrieve(authenticated_api_client, user, project: Project): + # Not found because user is not added to project + resp = authenticated_api_client.get(f'/api/v1/projects/{project.id}/') + assert resp.status_code == 404 + + project.add_followers([user]) + resp = authenticated_api_client.get(f'/api/v1/projects/{project.id}/') + + assert resp.json()['owner']['id'] + assert not resp.json()['collaborators'] + + followers = resp.json()['followers'] + assert len(followers) == 1 + assert followers[0]['id'] == user.id + + assert resp.json()['datasets'] == [] + assert resp.json()['item_counts'] == { + 'datasets': 0, + 'charts': 0, + 'simulations': 0, + } + + +@pytest.mark.django_db +def test_rest_project_set_permissions_not_allowed(authenticated_api_client, user, project: Project): + resp = authenticated_api_client.put( + f'/api/v1/projects/{project.id}/permissions/', + { + 'owner_id': user.id, + 'collaborator_ids': [], + 'follower_ids': [], + }, + ) + # 404 because user is not added to the project at all + assert resp.status_code == 404 + + project.add_followers([user]) + resp = authenticated_api_client.put( + f'/api/v1/projects/{project.id}/permissions/', + { + 'owner_id': user.id, + 'collaborator_ids': [], + 'follower_ids': [], + }, + ) + # User is added, but without sufficient perms, so 403 is returned + assert resp.status_code == 403 + + +@pytest.mark.django_db +def test_rest_project_set_permissions_change_owner_collaborator( + authenticated_api_client, user, project: Project +): + project.add_collaborators([user]) + resp = authenticated_api_client.put( + f'/api/v1/projects/{project.id}/permissions/', + { + 'owner_id': user.id, + 'collaborator_ids': [], + 'follower_ids': [], + }, + ) + assert resp.status_code == 403 + + +@pytest.mark.django_db +def test_rest_project_set_permissions_change_owner(api_client, user, project: Project): + owner = project.owner() + api_client.force_authenticate(user=owner) + resp = api_client.put( + f'/api/v1/projects/{project.id}/permissions/', + { + 'owner_id': user.id, + 'collaborator_ids': [owner.id], + 'follower_ids': [], + }, + ) + assert resp.status_code == 200 + assert resp.json()['owner']['id'] == user.id + assert resp.json()['collaborators'][0]['id'] == owner.id + + +@pytest.mark.django_db +def test_rest_project_delete(authenticated_api_client, user, project: Project): + resp = authenticated_api_client.delete(f'/api/v1/projects/{project.id}/') + assert resp.status_code == 404 + + project.add_followers([user]) + resp = authenticated_api_client.delete(f'/api/v1/projects/{project.id}/') + assert resp.status_code == 403 + + project.add_collaborators([user]) + resp = authenticated_api_client.delete(f'/api/v1/projects/{project.id}/') + assert resp.status_code == 403 + + project.set_owner(user) + resp = authenticated_api_client.delete(f'/api/v1/projects/{project.id}/') + assert resp.status_code == 204 diff --git a/uvdat/settings.py b/uvdat/settings.py index 60c38343..8899facb 100644 --- a/uvdat/settings.py +++ b/uvdat/settings.py @@ -74,7 +74,9 @@ class DevelopmentConfiguration(UvdatMixin, DevelopmentBaseConfiguration): class TestingConfiguration(UvdatMixin, TestingBaseConfiguration): - pass + # Ensure celery tasks run synchronously + CELERY_TASK_EAGER_PROPAGATES = True + CELERY_TASK_ALWAYS_EAGER = True class ProductionConfiguration(UvdatMixin, ProductionBaseConfiguration): diff --git a/uvdat/urls.py b/uvdat/urls.py index 407ee2dc..aa6293f1 100644 --- a/uvdat/urls.py +++ b/uvdat/urls.py @@ -9,10 +9,6 @@ from uvdat.core.rest import ( ChartViewSet, DatasetViewSet, - FileItemViewSet, - NetworkEdgeViewSet, - NetworkNodeViewSet, - NetworkViewSet, ProjectViewSet, RasterMapLayerViewSet, SimulationViewSet, @@ -32,13 +28,9 @@ router.register(r'users', UserViewSet, basename='users') router.register(r'projects', ProjectViewSet, basename='projects') router.register(r'datasets', DatasetViewSet, basename='datasets') -router.register(r'files', FileItemViewSet, basename='files') router.register(r'charts', ChartViewSet, basename='charts') router.register(r'rasters', RasterMapLayerViewSet, basename='rasters') router.register(r'vectors', VectorMapLayerViewSet, basename='vectors') -router.register(r'networks', NetworkViewSet, basename='networks') -router.register(r'nodes', NetworkNodeViewSet, basename='nodes') -router.register(r'edges', NetworkEdgeViewSet, basename='edges') router.register(r'source-regions', SourceRegionViewSet, basename='source-regions') router.register(r'simulations', SimulationViewSet, basename='simulations') diff --git a/web/src/api/rest.ts b/web/src/api/rest.ts index d4593bbc..d8925e75 100644 --- a/web/src/api/rest.ts +++ b/web/src/api/rest.ts @@ -90,10 +90,6 @@ export async function getDatasetLayers( return layers.toSorted((a, b) => a.index - b.index); } -export async function convertDataset(datasetId: number): Promise { - return (await apiClient.get(`datasets/${datasetId}/convert`)).data; -} - export async function getDatasetNetwork(datasetId: number): Promise { return (await apiClient.get(`datasets/${datasetId}/network`)).data; }