From 2f75eec01081c7d34c04a05854a0f08c6c31be9d Mon Sep 17 00:00:00 2001 From: Irwan Fathurrahman Date: Thu, 30 May 2024 13:46:34 +0700 Subject: [PATCH] Add style defaults --- deployment/docker/requirements.txt | 1 + .../context_layer_management/admin/layer.py | 11 +++- .../context_layer_management/api/base.py | 16 ++++-- .../context_layer_management/api/layer.py | 54 ++++++++++++++++++- .../context_layer_management/forms/style.py | 20 +++++++ .../migrations/0001_initial.py | 4 +- .../context_layer_management/models/layer.py | 10 +++- .../models/layer_upload.py | 24 ++++++++- .../models/style_defaults.py | 47 ++++++++++++++++ .../serializer/style.py | 46 ++++++++++++++++ .../context_layer_management/urls.py | 13 ++++- 11 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 django_project/context_layer_management/forms/style.py create mode 100644 django_project/context_layer_management/models/style_defaults.py create mode 100644 django_project/context_layer_management/serializer/style.py diff --git a/deployment/docker/requirements.txt b/deployment/docker/requirements.txt index 2fce376..acf2283 100644 --- a/deployment/docker/requirements.txt +++ b/deployment/docker/requirements.txt @@ -2,6 +2,7 @@ Django==4.2.7 # Web APIs for Django djangorestframework==3.14.0 +drf-nested-routers==0.93.5 # Geographic add-ons for Django Rest Framework djangorestframework-gis==1.0 diff --git a/django_project/context_layer_management/admin/layer.py b/django_project/context_layer_management/admin/layer.py index 5f16bf2..7936d00 100644 --- a/django_project/context_layer_management/admin/layer.py +++ b/django_project/context_layer_management/admin/layer.py @@ -4,6 +4,7 @@ from django.contrib import admin from context_layer_management.forms.layer import LayerForm, LayerUploadForm +from context_layer_management.forms.style import LayerStyleForm from context_layer_management.models.layer import Layer, LayerField, LayerStyle from context_layer_management.models.layer_upload import LayerUpload from context_layer_management.tasks import import_data @@ -63,8 +64,9 @@ class LayerUploadAdmin(admin.ModelAdmin): """Layer admin.""" list_display = ( - 'layer', 'created_by', 'created_at', 'folder', 'status', 'note' + 'created_at', 'created_by', 'layer', 'status', 'progress', 'note' ) + list_filter = ['layer', 'status'] actions = [start_upload_data] form = LayerUploadForm @@ -78,10 +80,17 @@ def get_form(self, request, *args, **kwargs): class LayerStyleAdmin(admin.ModelAdmin): """Layer Style admin.""" + form = LayerStyleForm list_display = ( 'name', 'created_by', 'created_at' ) + def get_form(self, request, *args, **kwargs): + """Return form.""" + form = super(LayerStyleAdmin, self).get_form(request, *args, **kwargs) + form.user = request.user + return form + admin.site.register(Layer, LayerAdmin) admin.site.register(LayerStyle, LayerStyleAdmin) diff --git a/django_project/context_layer_management/api/base.py b/django_project/context_layer_management/api/base.py index 5c6d6df..2c18dbc 100644 --- a/django_project/context_layer_management/api/base.py +++ b/django_project/context_layer_management/api/base.py @@ -12,15 +12,12 @@ from rest_framework.viewsets import mixins, GenericViewSet -class BaseApi( +class BaseReadApi( mixins.ListModelMixin, - mixins.CreateModelMixin, mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, GenericViewSet ): - """Base API for Resource.""" - + """Base Read API View.""" form_class = None lookup_field = 'id' @@ -83,6 +80,15 @@ def retrieve(self, request, *args, **kwargs): serializer = self.get_serializer(instance) return Response(serializer.data) + +class BaseApi( + BaseReadApi, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + GenericViewSet +): + """Base API for Resource.""" + def create(self, request, *args, **kwargs): """Update an object.""" data = request.data.copy() diff --git a/django_project/context_layer_management/api/layer.py b/django_project/context_layer_management/api/layer.py index 67e0606..a4b1e12 100644 --- a/django_project/context_layer_management/api/layer.py +++ b/django_project/context_layer_management/api/layer.py @@ -1,10 +1,16 @@ # coding=utf-8 """Context Layer Management.""" -from context_layer_management.api.base import BaseApi +from django.http import Http404 +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response + +from context_layer_management.api.base import BaseApi, BaseReadApi from context_layer_management.forms.layer import LayerForm -from context_layer_management.models.layer import Layer +from context_layer_management.forms.style import LayerStyleForm +from context_layer_management.models.layer import Layer, LayerStyle from context_layer_management.serializer.layer import LayerSerializer +from context_layer_management.serializer.style import StyleOfLayerSerializer class LayerViewSet(BaseApi): @@ -22,3 +28,47 @@ def get_serializer_context(self): 'view': self, 'user': self.request.user } + + +class StyleOfLayerViewSet(BaseReadApi): + """API layer style.""" + + form_class = LayerStyleForm + serializer_class = StyleOfLayerSerializer + + def _get_layer(self) -> Layer: # noqa: D102 + layer_id = self.kwargs.get('layer_id') + return get_object_or_404( + Layer.objects.filter(pk=layer_id) + ) + + def get_serializer_context(self): + """Extra context provided to the serializer class.""" + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self, + 'user': self.request.user, + 'layer': self._get_layer() + } + + def get_serializer(self, *args, **kwargs): + """Return the serializer instance.""" + serializer_class = self.get_serializer_class() + kwargs.setdefault('context', self.get_serializer_context()) + return serializer_class(*args, **kwargs) + + def get_queryset(self): + """Return queryset of API.""" + layer = self._get_layer() + ids = layer.styles.values_list('id', flat=True) + return LayerStyle.objects.filter(id__in=ids) + + def list(self, request, *args, **kwargs): + """Return just default style.""" + layer = self._get_layer() + if layer.default_style: + serializer = self.get_serializer(layer.default_style) + return Response(serializer.data) + else: + raise Http404 diff --git a/django_project/context_layer_management/forms/style.py b/django_project/context_layer_management/forms/style.py new file mode 100644 index 0000000..0e6af76 --- /dev/null +++ b/django_project/context_layer_management/forms/style.py @@ -0,0 +1,20 @@ +# coding=utf-8 +"""Context Layer Management.""" + +from django import forms + +from context_layer_management.models import LayerStyle + + +class LayerStyleForm(forms.ModelForm): + """Layer style form.""" + + def save(self, commit=True): + """Save the data.""" + if not self.instance.created_by_id: + self.instance.created_by_id = self.user.pk + return super(LayerStyleForm, self).save(commit=commit) + + class Meta: # noqa: D106 + model = LayerStyle + exclude = () diff --git a/django_project/context_layer_management/migrations/0001_initial.py b/django_project/context_layer_management/migrations/0001_initial.py index 44cd827..94d0e8e 100644 --- a/django_project/context_layer_management/migrations/0001_initial.py +++ b/django_project/context_layer_management/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-05-30 03:12 +# Generated by Django 4.2.7 on 2024-05-30 06:26 import context_layer_management.models.layer_upload from django.conf import settings @@ -58,7 +58,7 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, null=True)), ('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)), ('style', models.JSONField(help_text='Contains mapbox style information.')), - ('created_by', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ 'abstract': False, diff --git a/django_project/context_layer_management/models/layer.py b/django_project/context_layer_management/models/layer.py index a6c1b85..0d0410f 100644 --- a/django_project/context_layer_management/models/layer.py +++ b/django_project/context_layer_management/models/layer.py @@ -5,7 +5,8 @@ import uuid from django.conf import settings -from django.db import models, connection +from django.contrib.auth import get_user_model +from django.db import connection, models from django.db.models.signals import post_delete from django.dispatch import receiver from django.urls import reverse @@ -23,6 +24,8 @@ settings.MEDIA_URL, FOLDER_FILES ) +User = get_user_model() + class LayerType(object): """A quick couple of variable and Layer type.""" @@ -34,6 +37,11 @@ class LayerType(object): class LayerStyle(AbstractTerm, AbstractResource): """Model contains layer information.""" + created_by = models.ForeignKey( + User, on_delete=models.CASCADE, + editable=False, null=True, blank=True + ) + style = models.JSONField( help_text=( 'Contains mapbox style information.' diff --git a/django_project/context_layer_management/models/layer_upload.py b/django_project/context_layer_management/models/layer_upload.py index d195b8d..8cdc2c6 100644 --- a/django_project/context_layer_management/models/layer_upload.py +++ b/django_project/context_layer_management/models/layer_upload.py @@ -11,7 +11,10 @@ from django.dispatch import receiver from context_layer_management.models.general import AbstractResource -from context_layer_management.models.layer import Layer, LayerField +from context_layer_management.models.layer import Layer, LayerField, LayerStyle +from context_layer_management.models.style_defaults import ( + LINE, POINT, POLYGON +) from context_layer_management.tasks import import_data from context_layer_management.utils.connection import fields from context_layer_management.utils.geopandas import shapefile_to_postgis @@ -156,6 +159,23 @@ def import_data(self): layer.is_ready = True layer.metadata = metadata + + # Update default style + geometry_type = metadata['GEOMETRY TYPE'].lower() + if not layer.default_style_id: + default_style = POINT + if 'line' in geometry_type: + default_style = LINE + elif 'polygon' in geometry_type: + default_style = POLYGON + style, _ = LayerStyle.objects.get_or_create( + name=f'Default {geometry_type}', + defaults={ + 'style': default_style + } + ) + layer.default_style = style + layer.styles.add(style) layer.save() except Exception as e: # Save fields to layer @@ -170,7 +190,7 @@ def import_data(self): note='', progress=100 ) - self.delete_folder() + self.delete_folder() @receiver(post_delete, sender=LayerUpload) diff --git a/django_project/context_layer_management/models/style_defaults.py b/django_project/context_layer_management/models/style_defaults.py new file mode 100644 index 0000000..66416bc --- /dev/null +++ b/django_project/context_layer_management/models/style_defaults.py @@ -0,0 +1,47 @@ +POINT = { + "layers": [ + { + "id": "", + "type": "circle", + "paint": { + "circle-color": "#ff7800", + "circle-radius": 4, + "circle-opacity": 1 + }, + "filter": ["==", "$type", "Point"], + "source": "", + "source-layer": "default" + } + ] +} +LINE = { + "layers": [ + { + "id": "", + "type": "line", + "source": "", + "source-layer": "default", + "filter": ["==", "$type", "LineString"], + "paint": { + "line-color": "#ff7800", + "line-width": 1 + }, + } + ] +} + +POLYGON = { + "layers": [ + { + "id": "", + "type": "fill", + "source": "", + "source-layer": "default", + "filter": ["==", "$type", "Polygon"], + "paint": { + "fill-color": "#ff7800", + "fill-opacity": 0.8 + }, + } + ] +} diff --git a/django_project/context_layer_management/serializer/style.py b/django_project/context_layer_management/serializer/style.py new file mode 100644 index 0000000..68e1f87 --- /dev/null +++ b/django_project/context_layer_management/serializer/style.py @@ -0,0 +1,46 @@ +# coding=utf-8 +"""Context Layer Management.""" + +import json + +from rest_framework import serializers + +from context_layer_management.models.layer import LayerStyle + + +class StyleOfLayerSerializer(serializers.ModelSerializer): + """Serializer for layer.""" + + def get_style(self, obj: LayerStyle): + """Return style.""" + style = obj.style + layer = self.context.get('layer', None) + request = self.context.get('request', None) + if layer: + # Append uuid to style + if 'sources' not in style: + style['sources'] = {} + style['sources'][str(layer.unique_id)] = { + "tiles": [ + request.build_absolute_uri('/')[:-1] + layer.tile_url + ], + "type": "vector" + } + style = json.dumps(style).replace( + '', str(layer.unique_id) + ) + style = json.loads(style) + + # Append version + if 'version' not in style: + style['version'] = 8 + return style + + def to_representation(self, instance): + data = super(StyleOfLayerSerializer, self).to_representation(instance) + data.update(self.get_style(obj=instance)) + return data + + class Meta: # noqa: D106 + model = LayerStyle + fields = [] diff --git a/django_project/context_layer_management/urls.py b/django_project/context_layer_management/urls.py index 1ee2e23..1244e30 100644 --- a/django_project/context_layer_management/urls.py +++ b/django_project/context_layer_management/urls.py @@ -3,14 +3,24 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter +from rest_framework_nested.routers import NestedSimpleRouter -from context_layer_management.api.layer import LayerViewSet +from context_layer_management.api.layer import ( + LayerViewSet, StyleOfLayerViewSet +) from context_layer_management.api.vector_tile import (VectorTileLayer) router = DefaultRouter() router.register( r'layer', LayerViewSet, basename='context-layer-management-view-set' ) +layer_router = NestedSimpleRouter( + router, r'layer', lookup='layer' +) +layer_router.register( + 'style', StyleOfLayerViewSet, + basename='context-layer-management-style-view-set' +) urlpatterns = [ path( @@ -19,4 +29,5 @@ name='context-layer-management-tile-api' ), path('api/', include(router.urls)), + path('api/', include(layer_router.urls)), ]