From 1c33ddaa23790be68cbaf58d609ea40f4e7c3d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Cant=C3=B9?= Date: Thu, 21 Dec 2023 12:52:25 +0100 Subject: [PATCH] implement portals --- metadata_catalogue/maps/admin.py | 39 ++++++++++- metadata_catalogue/maps/api.py | 45 +++++++++++++ ...tal_portalmap_portal_portal_unique_uuid.py | 65 +++++++++++++++++++ metadata_catalogue/maps/models.py | 28 ++++++++ metadata_catalogue/maps/rules.py | 6 ++ metadata_catalogue/maps/schema.py | 14 ++++ 6 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 metadata_catalogue/maps/migrations/0005_portal_portalmap_portal_portal_unique_uuid.py diff --git a/metadata_catalogue/maps/admin.py b/metadata_catalogue/maps/admin.py index 1df87f6..a0d8eed 100644 --- a/metadata_catalogue/maps/admin.py +++ b/metadata_catalogue/maps/admin.py @@ -3,13 +3,17 @@ from treebeard.admin import TreeAdmin from treebeard.forms import movenodeform_factory -from .models import Layer, LayerGroup, Map, RasterSource, Source, VectorSource +from .models import Layer, LayerGroup, Map, Portal, PortalMap, RasterSource, Source, VectorSource class LayerInline(admin.TabularInline): model = Layer +class PortalMapInline(admin.TabularInline): + model = PortalMap + + @admin.register(LayerGroup) class LayerGroupAdmin(TreeAdmin): form = movenodeform_factory(LayerGroup) @@ -84,3 +88,36 @@ class LayerAdmin(admin.ModelAdmin): "group", "group_order", ] + + +@admin.register(Portal) +class PortalAdmin(admin.ModelAdmin): + list_display = [ + "id", + "title", + "visibility", + "uuid", + ] + + search_fields = [ + "title", + "uuid", + ] + + list_filter = [ + "visibility", + ] + + inlines = [ + PortalMapInline, + ] + + +@admin.register(PortalMap) +class PortalMapAdmin(admin.ModelAdmin): + list_display = [ + "id", + "portal", + "map", + "order", + ] diff --git a/metadata_catalogue/maps/api.py b/metadata_catalogue/maps/api.py index d44316a..ecfde2a 100644 --- a/metadata_catalogue/maps/api.py +++ b/metadata_catalogue/maps/api.py @@ -1,7 +1,12 @@ +import uuid +from typing import List + +from django.db.models import Q from ninja import Router from ninja.responses import codes_4xx from . import models, schema +from .enums import Visibility maps_router = Router() @@ -42,3 +47,43 @@ def get_map_style(request, map_slug: str): return 200, m.get_style(request) except models.Map.DoesNotExist: return 404, {"message": "Not found"} + + +@maps_router.get( + "/portals/{portal_uuid}/", + response={200: schema.Portal, codes_4xx: schema.StatusMessage}, + url_name="portals_get", + by_alias=True, + exclude_none=True, +) +def get_portal(request, portal_uuid: uuid.UUID): + try: + portal = models.Portal.objects.get(uuid=portal_uuid) + if not request.user.has_perm("maps.portal_view", portal): + return 404, {"message": "Not found"} + return 200, portal + except models.Map.DoesNotExist: + return 404, {"message": "Not found"} + + +@maps_router.get( + "/portals/{portal_uuid}/maps/", + response={200: list[schema.PortalMaps], codes_4xx: schema.StatusMessage}, + url_name="portals_get_maps", + exclude_none=True, +) +def get_portal_maps(request, portal_uuid: uuid.UUID): + try: + portal = models.Portal.objects.get(uuid=portal_uuid) + if not request.user.has_perm("maps.portal_view", portal): + return 404, {"message": "Not found"} + + expression = Q() + if request.user.is_authenticated: + if not request.user.is_staff: + expression = Q(map__visibility=Visibility.PUBLIC) | Q(map__owner=request.user) + else: + expression = Q(map__visibility=Visibility.PUBLIC) + return 200, portal.maps.filter(expression).select_related("map") + except models.Map.DoesNotExist: + return 404, {"message": "Not found"} diff --git a/metadata_catalogue/maps/migrations/0005_portal_portalmap_portal_portal_unique_uuid.py b/metadata_catalogue/maps/migrations/0005_portal_portalmap_portal_portal_unique_uuid.py new file mode 100644 index 0000000..8259190 --- /dev/null +++ b/metadata_catalogue/maps/migrations/0005_portal_portalmap_portal_portal_unique_uuid.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.8 on 2023-12-21 11:07 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import metadata_catalogue.maps.models +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("maps", "0004_map_visibility"), + ] + + operations = [ + migrations.CreateModel( + name="Portal", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("uuid", models.UUIDField(default=uuid.uuid4)), + ("title", models.CharField(max_length=250)), + ( + "visibility", + models.CharField( + choices=[("public", "Public"), ("private", "Private")], default="private", max_length=10 + ), + ), + ("extra", models.JSONField(blank=True, default=metadata_catalogue.maps.models.empty_json)), + ( + "owner", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="PortalMap", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("order", models.IntegerField(blank=True, default=0)), + ("extra", models.JSONField(blank=True, default=metadata_catalogue.maps.models.empty_json)), + ( + "map", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="portals", to="maps.map" + ), + ), + ( + "portal", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="maps", to="maps.portal" + ), + ), + ], + ), + migrations.AddConstraint( + model_name="portal", + constraint=models.UniqueConstraint(models.F("uuid"), name="portal_unique_uuid"), + ), + ] diff --git a/metadata_catalogue/maps/models.py b/metadata_catalogue/maps/models.py index 2c32d23..75295ff 100644 --- a/metadata_catalogue/maps/models.py +++ b/metadata_catalogue/maps/models.py @@ -1,3 +1,5 @@ +import uuid + from django.conf import settings from django.db import models from django.urls import reverse @@ -232,3 +234,29 @@ def as_layer_tree(self): ) return current_group + + +class Portal(models.Model): + uuid = models.UUIDField(default=uuid.uuid4) + title = models.CharField(max_length=250) + visibility = models.CharField(max_length=10, choices=Visibility.choices, default=Visibility.PRIVATE) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) + extra = models.JSONField(default=empty_json, blank=True) + + class Meta: + constraints = [ + models.UniqueConstraint("uuid", name="portal_unique_uuid"), + ] + + def __str__(self) -> str: + return self.title + + +class PortalMap(models.Model): + map = models.ForeignKey("maps.Map", on_delete=models.CASCADE, related_name="portals") + portal = models.ForeignKey("maps.Portal", on_delete=models.CASCADE, related_name="maps") + order = models.IntegerField(default=0, blank=True) + extra = models.JSONField(default=empty_json, blank=True) + + def __str__(self) -> str: + return f"{self.map} @ {self.portal}" diff --git a/metadata_catalogue/maps/rules.py b/metadata_catalogue/maps/rules.py index f33e7d1..965a66f 100644 --- a/metadata_catalogue/maps/rules.py +++ b/metadata_catalogue/maps/rules.py @@ -17,3 +17,9 @@ def is_public(user, object): rules.add_perm("maps.map_edit", is_owner | rules.is_staff) rules.add_perm("maps.map_add", is_owner | rules.is_staff) rules.add_perm("maps.map_delete", is_owner | rules.is_staff) + + +rules.add_perm("maps.portal_view", is_public | is_owner | rules.is_staff) +rules.add_perm("maps.portal_edit", is_owner | rules.is_staff) +rules.add_perm("maps.portal_add", is_owner | rules.is_staff) +rules.add_perm("maps.portal_delete", is_owner | rules.is_staff) diff --git a/metadata_catalogue/maps/schema.py b/metadata_catalogue/maps/schema.py index f5c1a2a..0a463ef 100644 --- a/metadata_catalogue/maps/schema.py +++ b/metadata_catalogue/maps/schema.py @@ -1,3 +1,4 @@ +import uuid from typing import Dict, List, Optional from ninja import Schema @@ -67,3 +68,16 @@ class MapMetadata(Schema): class StatusMessage(Schema): message: str + + +class PortalMaps(Schema): + slug: str = Field(None, alias="map.slug") + title: str = Field(None, alias="map.title") + description: str = Field(None, alias="map.description") + extra: JsonValue + + +class Portal(Schema): + title: str + uuid: uuid.UUID + extra: JsonValue