diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000000..52e0b802fa --- /dev/null +++ b/AUTHORS @@ -0,0 +1,7 @@ +Authors +======= + +Alessio Fabiani + +Contributors +============ diff --git a/README.md b/README.md index 63070631cd..f4614bfbae 100644 --- a/README.md +++ b/README.md @@ -443,7 +443,7 @@ Update your `GeoNode` > `settings.py` as follows: # To enable the MapStore2 based Client enable those if 'geonode_mapstore_client' not in INSTALLED_APPS: INSTALLED_APPS += ( - 'mapstore2_adapter', + 'geonode_mapstore_client.mapstore2_adapter', 'geonode_mapstore_client',) GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY = 'mapstore' # DEPRECATED use HOOKSET instead diff --git a/VERSION b/VERSION deleted file mode 100644 index fee3c9251d..0000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.0.10 \ No newline at end of file diff --git a/geonode_mapstore_client/apps.py b/geonode_mapstore_client/apps.py index 4b8729eebe..5115312928 100644 --- a/geonode_mapstore_client/apps.py +++ b/geonode_mapstore_client/apps.py @@ -9,6 +9,17 @@ # ######################################################################### from django.apps import AppConfig as BaseAppConfig +from django.utils.translation import ugettext_lazy as _ + + +def run_setup_hooks(*args, **kwargs): + from geonode.urls import urlpatterns + from django.conf.urls import url, include + + urlpatterns += [ + url(r'^mapstore/', include('mapstore2_adapter.urls')), + url(r'^', include('mapstore2_adapter.geoapps.geostories.api.urls')), + ] class AppConfig(BaseAppConfig): @@ -17,5 +28,5 @@ class AppConfig(BaseAppConfig): label = "geonode_mapstore_client" def ready(self): + run_setup_hooks() super(AppConfig, self).ready() - # run_setup_hooks() diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000000..4f5ea28a03 --- /dev/null +++ b/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,108 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-01-22 12:32+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: mapstore2_adapter/api/models.py:74 +msgid "String" +msgstr "" + +#: mapstore2_adapter/api/models.py:75 +msgid "Number" +msgstr "" + +#: mapstore2_adapter/api/models.py:76 +msgid "Integer" +msgstr "" + +#: mapstore2_adapter/api/models.py:77 +msgid "Boolean" +msgstr "" + +#: mapstore2_adapter/api/models.py:78 +msgid "Binary" +msgstr "" + +#: mapstore2_adapter/apps.py:30 +msgid "Django MapStore2 Adapter" +msgstr "" + +#: mapstore2_adapter/geoapps/apps.py:28 +msgid "MapStore2 Geonode Apps Plugins" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:32 +msgid "GeoStory Created" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:32 +msgid "A GeoStory was created" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:33 +msgid "GeoStory Updated" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:33 +msgid "A GeoStory was updated" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:34 +msgid "GeoStory Approved" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:34 +msgid "A GeoStory was approved by a Manager" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:35 +msgid "GeoStory Published" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:35 +msgid "A GeoStory was published" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:36 +msgid "GeoStory Deleted" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:36 +msgid "A GeoStory was deleted" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:37 +msgid "Comment on GeoStory" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:37 +msgid "A GeoStory was commented on" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:38 +msgid "Rating for GeoStory" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:38 +msgid "A rating was given to a GeoStory" +msgstr "" + +#: mapstore2_adapter/geoapps/geostories/models.py:36 +#, python-format +msgid "%s Type" +msgstr "" diff --git a/locale/it/LC_MESSAGES/django.mo b/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000000..8b40af255d Binary files /dev/null and b/locale/it/LC_MESSAGES/django.mo differ diff --git a/locale/it/LC_MESSAGES/django.po b/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000000..96b35ab292 --- /dev/null +++ b/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,108 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-01-22 12:32+0100\n" +"PO-Revision-Date: 2021-01-22 12:36+0100\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: \n" +"Language-Team: \n" +"X-Generator: Poedit 2.4.2\n" + +#: mapstore2_adapter/api/models.py:74 +msgid "String" +msgstr "Stringa" + +#: mapstore2_adapter/api/models.py:75 +msgid "Number" +msgstr "Numero" + +#: mapstore2_adapter/api/models.py:76 +msgid "Integer" +msgstr "Intero" + +#: mapstore2_adapter/api/models.py:77 +msgid "Boolean" +msgstr "Booleano" + +#: mapstore2_adapter/api/models.py:78 +msgid "Binary" +msgstr "Binario" + +#: mapstore2_adapter/apps.py:30 +msgid "Django MapStore2 Adapter" +msgstr "Django MapStore2 Adapter" + +#: mapstore2_adapter/geoapps/apps.py:28 +msgid "MapStore2 Geonode Apps Plugins" +msgstr "MapStore2 Geonode Apps Plugins" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:32 +msgid "GeoStory Created" +msgstr "GeoStory creata" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:32 +msgid "A GeoStory was created" +msgstr "È stata creata una GeoStory" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:33 +msgid "GeoStory Updated" +msgstr "GeoStory aggiornata" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:33 +msgid "A GeoStory was updated" +msgstr "Una GeoStory è stata aggiornata" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:34 +msgid "GeoStory Approved" +msgstr "GeoStory approvata" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:34 +msgid "A GeoStory was approved by a Manager" +msgstr "Una GeoStory è stata approvata da un geostore" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:35 +msgid "GeoStory Published" +msgstr "GeoStory pubblicata" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:35 +msgid "A GeoStory was published" +msgstr "È stata pubblicata una GeoStory" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:36 +msgid "GeoStory Deleted" +msgstr "GeoStory eliminata" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:36 +msgid "A GeoStory was deleted" +msgstr "Una GeoStory è stata eliminata" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:37 +msgid "Comment on GeoStory" +msgstr "Commento su una GeoStory" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:37 +msgid "A GeoStory was commented on" +msgstr "Una GeoStory è stata commentata" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:38 +msgid "Rating for GeoStory" +msgstr "Valutazione per GeoStories" + +#: mapstore2_adapter/geoapps/geostories/__init__.py:38 +msgid "A rating was given to a GeoStory" +msgstr "Una valutazione è stata data a una GeoStory" + +#: mapstore2_adapter/geoapps/geostories/models.py:36 +#, python-format +msgid "%s Type" +msgstr "%s Tipo" diff --git a/mapstore2_adapter/__init__.py b/mapstore2_adapter/__init__.py new file mode 100644 index 0000000000..37f44dc598 --- /dev/null +++ b/mapstore2_adapter/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### +import logging + +logger = logging.getLogger(__name__) + + +def fixup_map(map_id): + """ ------------------------------------- Maps Fix """ + from geonode.maps.models import Map + from django.contrib.auth import get_user_model + from mapstore2_adapter.api.models import MapStoreResource + for _m in Map.objects.filter(id=map_id): + try: + _u = get_user_model().objects.get(username=_m.owner.username) + _mm = MapStoreResource.objects.filter(id=_m.id) + if _mm.count(): + _mm = _mm.get() + else: + _mm = MapStoreResource.objects.create(id=_m.id, user_id=_u.id) + _mm.user = _u + _mm.save() + except Exception as e: + logger.exception(e) + + +class DjangoMapstore2AdapterBaseException(Exception): + """Base class for exceptions in this module.""" + pass + + +default_app_config = "mapstore2_adapter.apps.AppConfig" diff --git a/mapstore2_adapter/api/__init__.py b/mapstore2_adapter/api/__init__.py new file mode 100644 index 0000000000..0181a77bfa --- /dev/null +++ b/mapstore2_adapter/api/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### diff --git a/mapstore2_adapter/api/migrations/__init__.py b/mapstore2_adapter/api/migrations/__init__.py new file mode 100644 index 0000000000..0181a77bfa --- /dev/null +++ b/mapstore2_adapter/api/migrations/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### diff --git a/mapstore2_adapter/api/models.py b/mapstore2_adapter/api/models.py new file mode 100644 index 0000000000..e0c3c03293 --- /dev/null +++ b/mapstore2_adapter/api/models.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +import random +import logging + +from django.db import models +from django.contrib.auth import get_user_model +from django.utils.translation import ugettext_noop as _ + +from jsonfield import JSONField + +log = logging.getLogger(__name__) + + +def random_id(): + return random.randint(1000, 99999) + + +class MapStoreResource(models.Model): + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + id = models.BigIntegerField( + primary_key=True, + unique=True, + editable=True, + default=random_id) + name = models.CharField( + max_length=255, + unique=False, + blank=False, + null=False) + creation_date = models.DateTimeField( + null=True, + blank=True, + auto_now_add=True) + last_update = models.DateTimeField( + null=True, + blank=True, + auto_now=True) + data = models.OneToOneField( + "MapStoreData", + related_name="data", + null=True, + blank=True, + on_delete=models.CASCADE) + attributes = models.ManyToManyField( + "MapStoreAttribute", + related_name="attributes", + null=True, + blank=True) + + class Meta: + db_table = 'mapstore2_adapter_mapstoreresource' + indexes = [ + models.Index(fields=['id', ]), + models.Index(fields=['name', ]), + ] + + +class MapStoreAttribute(models.Model): + TYPE_STRING = 'string' + TYPE_NUMBER = 'number' + TYPE_INTEGER = 'integer' + TYPE_BOOLEAN = 'boolean' + TYPE_BINARY = 'binary' + + TYPES = ((TYPE_STRING, _("String"),), + (TYPE_NUMBER, _("Number"),), + (TYPE_INTEGER, _("Integer",),), + (TYPE_BOOLEAN, _("Boolean",),), + (TYPE_BINARY, _("Binary",),), + ) + + name = models.CharField( + max_length=255, + unique=False, + blank=False, + null=False) + label = models.CharField( + max_length=255, + unique=False, + blank=True, + null=True) + type = models.CharField( + max_length=80, + unique=False, + blank=False, + null=False, + choices=TYPES) + value = models.TextField( + db_column='value', + blank=True) + resource = models.ForeignKey( + MapStoreResource, + null=False, + blank=False, + on_delete=models.CASCADE) + + class Meta: + db_table = 'mapstore2_adapter_mapstoreattribute' + + +class MapStoreData(models.Model): + blob = JSONField( + null=False, + default={}) + resource = models.ForeignKey( + MapStoreResource, + null=False, + blank=False, + on_delete=models.CASCADE) + + class Meta: + db_table = 'mapstore2_adapter_mapstoredata' diff --git a/mapstore2_adapter/api/serializers.py b/mapstore2_adapter/api/serializers.py new file mode 100644 index 0000000000..266dfe2843 --- /dev/null +++ b/mapstore2_adapter/api/serializers.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +from django.contrib.auth import get_user_model +from rest_framework import serializers + +from .models import MapStoreResource + +import re +import six +import json +import base64 +import logging + +logger = logging.getLogger(__name__) + + +class JSONSerializerField(serializers.Field): + """ Serializer for JSONField -- required to make field writable""" + id = serializers.ReadOnlyField() + + def to_internal_value(self, data): + return data + + def to_representation(self, value): + data = value.blob if value and hasattr(value, 'blob') else value + if isinstance(data, six.string_types): + return json.loads(data) + return data + + +class JSONArraySerializerField(serializers.Field): + """ Serializer for JSONField -- required to make field writable""" + id = serializers.ReadOnlyField() + + def to_internal_value(self, data): + return data + + def to_representation(self, value): + if value: + attributes = [] + for _a in list(value.all()): + data = '' + if re.match(r'b\'(.*)\'', _a.value): + data = re.match(r'b\'(.*)\'', _a.value).groups()[0] + attributes.append({ + "name": _a.name, + "type": _a.type, + "label": _a.label, + "value": base64.b64decode(data).decode('utf8') + }) + else: + attributes = [] + return attributes + + +class MapLayersJSONArraySerializerField(serializers.Field): + + def to_internal_value(self, data): + return data + + def to_representation(self, value): + if value: + from geonode.maps.models import Map + from geonode.maps.api.serializers import MapLayerSerializer + map = Map.objects.get(id=value) + return MapLayerSerializer(embed=True, many=True).to_representation(map.layers) + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = get_user_model() + fields = ('url', 'username', 'email', 'is_staff', 'is_active', 'is_superuser',) + + +class MapStoreResourceSerializer(serializers.HyperlinkedModelSerializer): + user = serializers.CharField(source='user.username', + read_only=True) + layers = MapLayersJSONArraySerializerField(source='id', + read_only=True) + + def __init__(self, *args, **kwargs): + # Instantiate the superclass normally + super(MapStoreResourceSerializer, self).__init__(*args, **kwargs) + + _full = self.context['request'].query_params.get('full') + if _full: + self.fields['data'] = JSONSerializerField(read_only=False) + self.fields['attributes'] = JSONArraySerializerField(read_only=False) + + class Meta: + model = MapStoreResource + fields = ('id', 'user', 'layers', 'name', 'creation_date', 'last_update') diff --git a/mapstore2_adapter/api/urls.py b/mapstore2_adapter/api/urls.py new file mode 100644 index 0000000000..ae51a72305 --- /dev/null +++ b/mapstore2_adapter/api/urls.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +from django.conf.urls import url, include +from rest_framework import routers +from . import views + +router = routers.DefaultRouter() +router.register(r'users', views.UserViewSet) +router.register(r'resources', views.MapStoreResourceViewSet, basename="resources") + +urlpatterns = [ + url(r'^rest/', include(router.urls)), + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) +] diff --git a/mapstore2_adapter/api/views.py b/mapstore2_adapter/api/views.py new file mode 100644 index 0000000000..38a198b4fd --- /dev/null +++ b/mapstore2_adapter/api/views.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +from django.contrib.auth import get_user_model + +from rest_framework import viewsets +from rest_framework.authentication import SessionAuthentication, BasicAuthentication +from rest_framework.permissions import IsAdminUser, IsAuthenticated, IsAuthenticatedOrReadOnly # noqa +from oauth2_provider.contrib.rest_framework import OAuth2Authentication +from geonode.base.api.permissions import IsOwnerOrReadOnly + +from .models import MapStoreResource +from .serializers import (UserSerializer, + MapStoreResourceSerializer,) +from ..hooks import hookset + +import logging + +logger = logging.getLogger(__name__) + + +class UserViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows users to be viewed or edited. + """ + authentication_classes = (SessionAuthentication, BasicAuthentication, OAuth2Authentication) + permission_classes = (IsAdminUser,) + queryset = get_user_model().objects.all() + serializer_class = UserSerializer + + +class MapStoreResourceViewSet(viewsets.ModelViewSet): + """ Only Authenticate User perform CRUD Operations on Respective Data + """ + authentication_classes = [SessionAuthentication, BasicAuthentication, OAuth2Authentication] + permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] + model = MapStoreResource + serializer_class = MapStoreResourceSerializer + + def get_queryset(self): + """ Return datasets belonging to the current user """ + queryset = self.model.objects.all() + + # filter to tasks owned by user making request + queryset = hookset.get_queryset(self, queryset) + return queryset + + def perform_create(self, serializer): + """ Associate current user as task owner """ + if serializer.is_valid(): + hookset.perform_create(self, serializer) + return serializer.save(user=self.request.user) + + def perform_update(self, serializer): + """ Associate current user as task owner """ + if serializer.is_valid(): + hookset.perform_update(self, serializer) + return serializer.save() diff --git a/mapstore2_adapter/apps.py b/mapstore2_adapter/apps.py new file mode 100644 index 0000000000..3091b1d9b2 --- /dev/null +++ b/mapstore2_adapter/apps.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### +from django.apps import AppConfig as BaseAppConfig +from django.utils.translation import ugettext_lazy as _ + + +class AppConfig(BaseAppConfig): + + name = "mapstore2_adapter" + label = "mapstore2_adapter" + verbose_name = _("Django MapStore2 Adapter") + + def ready(self): + """Finalize setup""" + # run_setup_hooks() + super(AppConfig, self).ready() diff --git a/mapstore2_adapter/conf.py b/mapstore2_adapter/conf.py new file mode 100644 index 0000000000..7355ee8158 --- /dev/null +++ b/mapstore2_adapter/conf.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +import importlib + +from django.conf import settings # noqa +from django.core.exceptions import ImproperlyConfigured + +from appconf import AppConf + + +def load_path_attr(path): + i = path.rfind(".") + module, attr = path[:i], path[i + 1:] + try: + mod = importlib.import_module(module) + except ImportError as e: + raise ImproperlyConfigured("Error importing {0}: '{1}'".format(module, e)) + try: + attr = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured("Module '{0}' does not define a '{1}'".format(module, attr)) + return attr + + +def is_installed(package): + try: + __import__(package) + return True + except ImportError: + return False + + +class DjangoMapstore2AdapterAppConf(AppConf): + + SERIALIZER = "mapstore2_adapter.plugins.serializers.GeoStoreSerializer" + + def configure_hookset(self, value): + return load_path_attr(value)() + + class Meta: + prefix = "mapstore2_adapter" diff --git a/mapstore2_adapter/context_processors.py b/mapstore2_adapter/context_processors.py new file mode 100644 index 0000000000..111ed065cd --- /dev/null +++ b/mapstore2_adapter/context_processors.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +from django.conf import settings + + +def resource_urls(request): + """Global values to pass to templates""" + defaults = dict( + MAP_BASELAYERS=getattr(settings, "MAPSTORE_BASELAYERS", []), + CATALOGUE_SERVICES=getattr(settings, "MAPSTORE_CATALOGUE_SERVICES", {}), + CATALOGUE_SELECTED_SERVICE=getattr(settings, "MAPSTORE_CATALOGUE_SELECTED_SERVICE", None), + ) + + return defaults diff --git a/mapstore2_adapter/converters.py b/mapstore2_adapter/converters.py new file mode 100644 index 0000000000..2c2a5170fd --- /dev/null +++ b/mapstore2_adapter/converters.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +import logging + +logger = logging.getLogger(__name__) + + +class BaseMapStore2ConfigConverter(object): + + def convert(self, viewer, request): + """ + return { + "version": 2, + "widgetsConfig": { + ... + } + "config": { + ... + } + } + """ + raise NotImplementedError() + + def get_overlays(self, viewer): + """ + return (overlays, selected) + """ + raise NotImplementedError() + + def get_center_and_zoom(self, view_map, overlay): + """ + return (center_xy, zoom_level) + """ + raise NotImplementedError() + + def viewer_json(self, viewer, request): + raise NotImplementedError() diff --git a/mapstore2_adapter/geoapps/__init__.py b/mapstore2_adapter/geoapps/__init__.py new file mode 100644 index 0000000000..fe2f013b66 --- /dev/null +++ b/mapstore2_adapter/geoapps/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from pkgutil import extend_path + + +default_app_config = "mapstore2_adapter.geoapps.apps.AppConfig" +__path__ = extend_path(__path__, __name__) # noqa diff --git a/mapstore2_adapter/geoapps/apps.py b/mapstore2_adapter/geoapps/apps.py new file mode 100644 index 0000000000..122dd42ecc --- /dev/null +++ b/mapstore2_adapter/geoapps/apps.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.apps import AppConfig as BaseAppConfig +from django.utils.translation import ugettext_lazy as _ + + +class AppConfig(BaseAppConfig): + + name = "mapstore2_adapter.geoapps" + label = "mapstore2_adapter_geoapps" + verbose_name = _("MapStore2 Geonode Apps Plugins") diff --git a/mapstore2_adapter/geoapps/geostories/__init__.py b/mapstore2_adapter/geoapps/geostories/__init__.py new file mode 100644 index 0000000000..de7af57f2b --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/__init__.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.utils.translation import ugettext_noop as _ +from geonode.geoapps import GeoNodeAppsConfig + + +class GeoStoryAppsConfig(GeoNodeAppsConfig): + + name = 'mapstore2_adapter.geoapps.geostories' + label = "geoapp_geostories" + default_model = 'GeoStory' + verbose_name = "GeoNode App: GeoStory" + type = 'GEONODE_APP' + + NOTIFICATIONS = (("geostory_created", _("GeoStory Created"), _("A GeoStory was created"),), + ("geostory_updated", _("GeoStory Updated"), _("A GeoStory was updated"),), + ("geostory_approved", _("GeoStory Approved"), _("A GeoStory was approved by a Manager"),), + ("geostory_published", _("GeoStory Published"), _("A GeoStory was published"),), + ("geostory_deleted", _("GeoStory Deleted"), _("A GeoStory was deleted"),), + ("geostory_comment", _("Comment on GeoStory"), _("A GeoStory was commented on"),), + ("geostory_rated", _("Rating for GeoStory"), _("A rating was given to a GeoStory"),), + ) + + +default_app_config = "mapstore2_adapter.geoapps.geostories.GeoStoryAppsConfig" diff --git a/mapstore2_adapter/geoapps/geostories/api/__init__.py b/mapstore2_adapter/geoapps/geostories/api/__init__.py new file mode 100644 index 0000000000..fe4e643c90 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/api/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### diff --git a/mapstore2_adapter/geoapps/geostories/api/permissions.py b/mapstore2_adapter/geoapps/geostories/api/permissions.py new file mode 100644 index 0000000000..0e74988823 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/api/permissions.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.conf import settings +from rest_framework.filters import BaseFilterBackend + + +class GeoStoryPermissionsFilter(BaseFilterBackend): + """ + A filter backend that limits results to those where the requesting user + has read object level permissions. + """ + shortcut_kwargs = { + 'accept_global_perms': True, + } + + def filter_queryset(self, request, queryset, view): + # We want to defer this import until runtime, rather than import-time. + # See https://github.com/encode/django-rest-framework/issues/4608 + # (Also see #1624 for why we need to make this import explicitly) + from guardian.shortcuts import get_objects_for_user + from geonode.security.utils import get_visible_resources + + user = request.user + resources = get_objects_for_user( + user, + 'base.view_resourcebase', + **self.shortcut_kwargs + ).filter(polymorphic_ctype__model='geostory') + + obj_with_perms = get_visible_resources( + resources, + user, + admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, + unpublished_not_visible=settings.RESOURCE_PUBLISHING, + private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES) + + return queryset.filter(id__in=obj_with_perms.values('id')) diff --git a/mapstore2_adapter/geoapps/geostories/api/serializers.py b/mapstore2_adapter/geoapps/geostories/api/serializers.py new file mode 100644 index 0000000000..a0693c2269 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/api/serializers.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +import json +import logging + +from django.contrib.auth import get_user_model + +from rest_framework.serializers import ValidationError + +from dynamic_rest.serializers import DynamicModelSerializer +from dynamic_rest.fields.fields import DynamicRelationField + +from geonode.geoapps.models import GeoAppData +from geonode.base.api.serializers import ResourceBaseSerializer + +from ..models import GeoStory + +logger = logging.getLogger(__name__) + + +class GeoAppDataField(DynamicRelationField): + + def value_to_string(self, obj): + value = self.value_from_object(obj) + return self.get_prep_value(value) + + +class GeoAppDataSerializer(DynamicModelSerializer): + + class Meta: + ref_name = 'GeoAppData' + model = GeoAppData + name = 'GeoAppData' + fields = ('pk', 'blob') + + def to_internal_value(self, data): + return data + + def to_representation(self, value): + data = GeoAppData.objects.filter(resource__id=value).first() + return json.loads(data.blob) if data else {} + + +class GeoStorySerializer(ResourceBaseSerializer): + + def __init__(self, *args, **kwargs): + # Instantiate the superclass normally + super(GeoStorySerializer, self).__init__(*args, **kwargs) + + class Meta: + model = GeoStory + name = 'geostory' + fields = ( + 'pk', 'uuid', 'app_type', + 'zoom', 'projection', 'center_x', 'center_y', + 'urlsuffix', 'data' + ) + + def to_internal_value(self, data): + if 'data' in data: + _data = data.pop('data') + if self.is_valid(): + data['blob'] = _data + + return data + + def create(self, validated_data): + # Sanity checks + if 'name' not in validated_data or \ + 'owner' not in validated_data: + raise ValidationError("No valid data: 'name' and 'owner' are mandatory fields!") + + if GeoStory.objects.filter(name=validated_data['name']).count(): + raise ValidationError("A GeoApp with the same 'name' already exists!") + + # Extract users' profiles + _user_profiles = {} + for _key, _value in validated_data.items(): + if _key in ('owner', 'poc', 'metadata_owner'): + _user_profiles[_key] = _value + for _key, _value in _user_profiles.items(): + validated_data.pop(_key) + _u = get_user_model().objects.filter(username=_value).first() + if _u: + validated_data[_key] = _u + else: + raise ValidationError("The specified '{}' does not exist!".format(_key)) + + # Extract JSON blob + _data = None + if 'blob' in validated_data: + _data = validated_data.pop('blob') + + # Create a new instance + _instance = GeoStory.objects.create(**validated_data) + + if _instance and _data: + try: + _geo_app, _created = GeoAppData.objects.get_or_create(resource=_instance) + _geo_app.blob = _data + _geo_app.save() + except Exception as e: + raise ValidationError(e) + + _instance.save() + return _instance + + def update(self, instance, validated_data): + + # Extract users' profiles + _user_profiles = {} + for _key, _value in validated_data.items(): + if _key in ('owner', 'poc', 'metadata_owner'): + _user_profiles[_key] = _value + for _key, _value in _user_profiles.items(): + validated_data.pop(_key) + _u = get_user_model().objects.filter(username=_value).first() + if _u: + validated_data[_key] = _u + else: + raise ValidationError("The specified '{}' does not exist!".format(_key)) + + # Extract JSON blob + _data = None + if 'blob' in validated_data: + _data = validated_data.pop('blob') + + try: + GeoStory.objects.filter(pk=instance.id).update(**validated_data) + instance.refresh_from_db() + except Exception as e: + raise ValidationError(e) + + if instance and _data: + try: + _geo_app, _created = GeoAppData.objects.get_or_create(resource=instance) + _geo_app.blob = _data + _geo_app.save() + except Exception as e: + raise ValidationError(e) + + instance.save() + return instance + + """ + - Deferred / not Embedded --> ?include[]=data + """ + data = GeoAppDataField( + GeoAppDataSerializer, + source='id', + many=False, + embed=False, + deferred=True) diff --git a/mapstore2_adapter/geoapps/geostories/api/urls.py b/mapstore2_adapter/geoapps/geostories/api/urls.py new file mode 100644 index 0000000000..301febe061 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/api/urls.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from django.conf.urls import url, include + +from geonode.api.urls import router + +from mapstore2_adapter.geoapps.geostories.api import views + +router.register(r'geostories', views.GeoStoryViewSet) + +urlpatterns = [ + url(r'^api/v2/', include(router.urls)), +] diff --git a/mapstore2_adapter/geoapps/geostories/api/views.py b/mapstore2_adapter/geoapps/geostories/api/views.py new file mode 100644 index 0000000000..53016ec702 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/api/views.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from dynamic_rest.viewsets import DynamicModelViewSet +from dynamic_rest.filters import DynamicFilterBackend, DynamicSortingFilter + +from rest_framework.permissions import IsAdminUser, IsAuthenticated, IsAuthenticatedOrReadOnly, DjangoModelPermissionsOrAnonReadOnly # noqa +from rest_framework.authentication import SessionAuthentication, BasicAuthentication +from oauth2_provider.contrib.rest_framework import OAuth2Authentication + +from geonode.base.api.filters import DynamicSearchFilter +from geonode.base.api.permissions import IsOwnerOrReadOnly +from geonode.base.api.pagination import GeoNodeApiPagination + +from mapstore2_adapter.geoapps.geostories.models import GeoStory +from mapstore2_adapter.geoapps.geostories.api.serializers import GeoStorySerializer +from mapstore2_adapter.geoapps.geostories.api.permissions import GeoStoryPermissionsFilter + +import logging + +logger = logging.getLogger(__name__) + + +class GeoStoryViewSet(DynamicModelViewSet): + """ + API endpoint that allows geoapps to be viewed or edited. + """ + authentication_classes = [SessionAuthentication, BasicAuthentication, OAuth2Authentication] + permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] + filter_backends = [DynamicFilterBackend, DynamicSortingFilter, DynamicSearchFilter, GeoStoryPermissionsFilter] + queryset = GeoStory.objects.all() + serializer_class = GeoStorySerializer + pagination_class = GeoNodeApiPagination diff --git a/mapstore2_adapter/geoapps/geostories/migrations/0001_initial.py b/mapstore2_adapter/geoapps/geostories/migrations/0001_initial.py new file mode 100644 index 0000000000..4f11171532 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.15 on 2020-10-13 12:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('geoapps', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='GeoStory', + fields=[ + ('geoapp_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='geoapps.GeoApp')), + ('_app_type', models.CharField(default='GeoStory', max_length=255, verbose_name='Apps Type')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('geoapps.geoapp',), + ), + ] diff --git a/mapstore2_adapter/geoapps/geostories/migrations/0002_auto_20201015_1533.py b/mapstore2_adapter/geoapps/geostories/migrations/0002_auto_20201015_1533.py new file mode 100644 index 0000000000..7c0017df70 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/migrations/0002_auto_20201015_1533.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.15 on 2020-10-15 15:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('geoapp_geostories', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='geostory', + name='_app_type', + ), + migrations.AddField( + model_name='geostory', + name='app_type', + field=models.CharField(db_column='geostory_app_type', default='GeoStory', max_length=255, verbose_name='Apps Type'), + ), + ] diff --git a/mapstore2_adapter/geoapps/geostories/migrations/__init__.py b/mapstore2_adapter/geoapps/geostories/migrations/__init__.py new file mode 100644 index 0000000000..fe4e643c90 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/migrations/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### diff --git a/mapstore2_adapter/geoapps/geostories/models.py b/mapstore2_adapter/geoapps/geostories/models.py new file mode 100644 index 0000000000..5773efb672 --- /dev/null +++ b/mapstore2_adapter/geoapps/geostories/models.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +import logging + +from django.db import models +from django.conf import settings +from django.db.models import signals +from django.utils.translation import ugettext_lazy as _ + +from geonode.geoapps.models import GeoApp +from geonode.base.models import resourcebase_post_save + +logger = logging.getLogger(__name__) + + +class GeoStory(GeoApp): + + app_type = models.CharField( + _('%s Type' % settings.GEONODE_APPS_NAME), + db_column='geostory_app_type', + default='GeoStory', + max_length=255) + # The type of the current geoapp. + + +signals.post_save.connect(resourcebase_post_save, sender=GeoStory) diff --git a/mapstore2_adapter/hooks.py b/mapstore2_adapter/hooks.py new file mode 100644 index 0000000000..1730ee4054 --- /dev/null +++ b/mapstore2_adapter/hooks.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +from .conf import settings +from six import string_types + + +class HookProxy(object): + + def __getattr__(self, attr): + if not isinstance(settings.MAPSTORE2_ADAPTER_SERIALIZER, string_types): + return getattr(settings.MAPSTORE2_ADAPTER_SERIALIZER, attr) + else: + import importlib + cls = settings.MAPSTORE2_ADAPTER_SERIALIZER.split(".") + module_name, class_name = (".".join(cls[:-1]), cls[-1]) + i = importlib.import_module(module_name) + hook = getattr(i, class_name)() + return getattr(hook, attr) + + +hookset = HookProxy() diff --git a/mapstore2_adapter/migrations/0001_initial.py b/mapstore2_adapter/migrations/0001_initial.py new file mode 100644 index 0000000000..10ee5c9793 --- /dev/null +++ b/mapstore2_adapter/migrations/0001_initial.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-09-19 09:49 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import jsonfield.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='MapStoreAttribute', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('label', models.CharField(blank=True, max_length=255, null=True)), + ('type', models.CharField(choices=[('string', b'String'), ('number', b'Number'), ('integer', b'Integer'), ('boolean', b'Boolean'), ('binary', b'Binary')], max_length=80)), + ('value', models.TextField(blank=True, db_column='value')), + ], + ), + migrations.CreateModel( + name='MapStoreData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('blob', jsonfield.fields.JSONField(default={})), + ], + ), + migrations.CreateModel( + name='MapStoreResource', + fields=[ + ('id', models.BigIntegerField(blank=True, null=True, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=255)), + ('creation_date', models.DateTimeField(auto_now_add=True, null=True)), + ('last_update', models.DateTimeField(auto_now=True, null=True)), + ('attributes', models.ManyToManyField(blank=True, null=True, related_name='attributes', + to='mapstore2_adapter.MapStoreAttribute')), + ('data', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='data', to='mapstore2_adapter.MapStoreData')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='mapstoredata', + name='resource', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + to='mapstore2_adapter.MapStoreResource'), + ), + migrations.AddField( + model_name='mapstoreattribute', + name='resource', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + to='mapstore2_adapter.MapStoreResource'), + ), + migrations.AddIndex( + model_name='mapstoreresource', + index=models.Index(fields=['id'], name='mapstore2_a_id_cd23a9_idx'), + ), + migrations.AddIndex( + model_name='mapstoreresource', + index=models.Index(fields=['name'], name='mapstore2_a_name_35c0a1_idx'), + ), + ] diff --git a/mapstore2_adapter/migrations/0002_auto_20190618_1236.py b/mapstore2_adapter/migrations/0002_auto_20190618_1236.py new file mode 100644 index 0000000000..2b47c45a24 --- /dev/null +++ b/mapstore2_adapter/migrations/0002_auto_20190618_1236.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2019-06-18 12:36 +from __future__ import unicode_literals + +from django.db import migrations, models +import mapstore2_adapter.api.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapstore2_adapter', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='mapstoreresource', + name='id', + field=models.BigIntegerField( + default=mapstore2_adapter.api.models.random_id, primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/mapstore2_adapter/migrations/0003_auto_20200310_0848.py b/mapstore2_adapter/migrations/0003_auto_20200310_0848.py new file mode 100644 index 0000000000..d1240e9f66 --- /dev/null +++ b/mapstore2_adapter/migrations/0003_auto_20200310_0848.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-10 08:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapstore2_adapter', '0002_auto_20190618_1236'), + ] + + operations = [ + migrations.AlterField( + model_name='mapstoreattribute', + name='type', + field=models.CharField(choices=[('string', 'String'), ('number', 'Number'), ('integer', 'Integer'), ('boolean', 'Boolean'), ('binary', 'Binary')], max_length=80), + ), + ] diff --git a/mapstore2_adapter/migrations/0004_auto_20210219_1015.py b/mapstore2_adapter/migrations/0004_auto_20210219_1015.py new file mode 100644 index 0000000000..09afe4bdae --- /dev/null +++ b/mapstore2_adapter/migrations/0004_auto_20210219_1015.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.16 on 2021-02-19 10:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapstore2_adapter', '0003_auto_20200310_0848'), + ] + + operations = [ + migrations.AlterModelTable( + name='mapstoreattribute', + table='mapstore2_adapter_mapstoreattribute', + ), + migrations.AlterModelTable( + name='mapstoredata', + table='mapstore2_adapter_mapstoredata', + ), + migrations.AlterModelTable( + name='mapstoreresource', + table='mapstore2_adapter_mapstoreresource', + ), + ] diff --git a/mapstore2_adapter/migrations/__init__.py b/mapstore2_adapter/migrations/__init__.py new file mode 100644 index 0000000000..0181a77bfa --- /dev/null +++ b/mapstore2_adapter/migrations/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### diff --git a/mapstore2_adapter/plugins/__init__.py b/mapstore2_adapter/plugins/__init__.py new file mode 100644 index 0000000000..0181a77bfa --- /dev/null +++ b/mapstore2_adapter/plugins/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### diff --git a/mapstore2_adapter/plugins/geonode.py b/mapstore2_adapter/plugins/geonode.py new file mode 100644 index 0000000000..36310ac80f --- /dev/null +++ b/mapstore2_adapter/plugins/geonode.py @@ -0,0 +1,568 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +from __future__ import absolute_import, unicode_literals + +from urllib import parse +from six import string_types + +try: + import json +except ImportError: + from django.utils import simplejson as json + +import logging +import traceback + +from ..utils import ( + GoogleZoom, + get_wfs_endpoint, + get_valid_number, + to_json) +from ..settings import ( + MAP_BASELAYERS, + CATALOGUE_SERVICES, + CATALOGUE_SELECTED_SERVICE) + +from ..converters import BaseMapStore2ConfigConverter + +from django.contrib.gis.geos import Polygon +from django.contrib.gis.gdal import SpatialReference, CoordTransform +from django.core.serializers.json import DjangoJSONEncoder +from django.conf import settings + + +logger = logging.getLogger(__name__) + +unsafe_chars = { + '&': '\\u0026', + '<': '\\u003c', + '>': '\\u003e', + '\u2028': '\\u2028', + '\u2029': '\\u2029' +} + + +class GeoNodeMapStore2ConfigConverter(BaseMapStore2ConfigConverter): + + def convert(self, viewer, request): + """ + input: GeoNode JSON Gxp Config + output: MapStore2 compliant str(config) + """ + # Initialization + viewer_obj = json.loads(viewer) + + map_id = None + if 'id' in viewer_obj and viewer_obj['id']: + try: + map_id = int(viewer_obj['id']) + except Exception: + pass + + data = {} + data['version'] = 2 + + # Map Definition + try: + # Map Definition + ms2_map = {} + ms2_map['projection'] = viewer_obj['map']['projection'] + ms2_map['units'] = viewer_obj['map']['units'] + ms2_map['zoom'] = viewer_obj['map']['zoom'] if viewer_obj['map']['zoom'] > 0 else 2 + ms2_map['maxExtent'] = viewer_obj['map']['maxExtent'] + ms2_map['maxResolution'] = viewer_obj['map']['maxResolution'] + + # Backgrouns + backgrounds = self.getBackgrounds(viewer, MAP_BASELAYERS) + if backgrounds: + ms2_map['layers'] = backgrounds + else: + ms2_map['layers'] = MAP_BASELAYERS + [ + # TODO: covnert Viewer Background Layers + # Add here more backgrounds e.g.: + # { + # "type": "wms", + # "url": "https://demo.geo-solutions.it/geoserver/wms", + # "visibility": True, + # "opacity": 0.5, + # "title": "Weather data", + # "name": "nurc:Arc_Sample", + # "group": "Meteo", + # "format": "image/png", + # "bbox": { + # "bounds": { + # "minx": -25.6640625, + # "miny": 26.194876675795218, + # "maxx": 48.1640625, + # "maxy": 56.80087831233043 + # }, + # "crs": "EPSG:4326" + # } + # }, ... + ] + + if settings.BING_API_KEY: + ms2_map['bingApiKey'] = settings.BING_API_KEY + + # Security Info + info = {} + info['canDelete'] = False + info['canEdit'] = False + info['description'] = viewer_obj['about']['abstract'] + info['id'] = map_id + info['name'] = viewer_obj['about']['title'] + ms2_map['info'] = info + + # Overlays + overlays, selected = self.get_overlays(viewer, request=request) + if selected and 'name' in selected and selected['name'] and not map_id: + # We are generating a Layer Details View + center, zoom = self.get_center_and_zoom(viewer_obj['map'], selected) + ms2_map['center'] = center + ms2_map['zoom'] = zoom + + try: + # - extract from GeoNode guardian + from geonode.layers.views import (_resolve_layer, + _PERMISSION_MSG_MODIFY, + _PERMISSION_MSG_DELETE) + if _resolve_layer(request, + selected['name'], + 'base.change_resourcebase', + _PERMISSION_MSG_MODIFY): + info['canEdit'] = True + + if _resolve_layer(request, + selected['name'], + 'base.delete_resourcebase', + _PERMISSION_MSG_DELETE): + info['canDelete'] = True + except Exception: + tb = traceback.format_exc() + logger.debug(tb) + else: + # We are getting the configuration of a Map + # On GeoNode model the Map Center is always saved in 4326 + _x = get_valid_number(viewer_obj['map']['center'][0]) + _y = get_valid_number(viewer_obj['map']['center'][1]) + _crs = 'EPSG:4326' + if _x > 360.0 or _x < -360.0: + _crs = viewer_obj['map']['projection'] + ms2_map['center'] = { + 'x': _x, + 'y': _y, + 'crs': _crs + } + try: + # - extract from GeoNode guardian + from geonode.maps.views import (_resolve_map, + _PERMISSION_MSG_SAVE, + _PERMISSION_MSG_DELETE) + if _resolve_map(request, + str(map_id), + 'base.change_resourcebase', + _PERMISSION_MSG_SAVE): + info['canEdit'] = True + + if _resolve_map(request, + str(map_id), + 'base.delete_resourcebase', + _PERMISSION_MSG_DELETE): + info['canDelete'] = True + except Exception: + tb = traceback.format_exc() + logger.debug(tb) + + for overlay in overlays: + if 'name' in overlay and overlay['name']: + ms2_map['layers'].append(overlay) + + data['map'] = ms2_map + except Exception: + # traceback.print_exc() + tb = traceback.format_exc() + logger.debug(tb) + + # Additional Configurations + if map_id: + from mapstore2_adapter import fixup_map + from mapstore2_adapter.api.models import MapStoreResource + try: + fixup_map(map_id) + ms2_resource = MapStoreResource.objects.get(id=map_id) + ms2_map_data = ms2_resource.data.blob + if isinstance(ms2_map_data, string_types): + ms2_map_data = json.loads(ms2_map_data) + if 'map' in ms2_map_data: + for _k, _v in ms2_map_data['map'].items(): + if _k not in data['map']: + data['map'][_k] = ms2_map_data['map'][_k] + del ms2_map_data['map'] + data.update(ms2_map_data) + except Exception: + # traceback.print_exc() + tb = traceback.format_exc() + logger.debug(tb) + + # Default Catalogue Services Definition + try: + ms2_catalogue = {} + ms2_catalogue['selectedService'] = CATALOGUE_SELECTED_SERVICE + ms2_catalogue['services'] = CATALOGUE_SERVICES + data['catalogServices'] = ms2_catalogue + except Exception: + # traceback.print_exc() + tb = traceback.format_exc() + logger.debug(tb) + + json_str = json.dumps(data, cls=DjangoJSONEncoder, sort_keys=True) + for (c, d) in unsafe_chars.items(): + json_str = json_str.replace(c, d) + + return json_str + + def getBackgrounds(self, viewer, defaults): + import copy + backgrounds = copy.deepcopy(defaults) + def_background = None + for bg in backgrounds: + if bg['visibility']: + def_background = bg + break + try: + viewer_obj = json.loads(viewer) + layers = viewer_obj['map']['layers'] + for bg in backgrounds: + bg['visibility'] = False + any_visible = False + for layer in layers: + if 'group' in layer and layer['group'] == "background" and layer['visibility']: + def_local_background = [bg for bg in backgrounds if bg['name'] == layer['name']] + def_background = def_local_background[0] if def_local_background else None + if def_background: + def_background['opacity'] = layer['opacity'] if 'opacity' in layer else 1.0 + def_background['visibility'] = True + any_visible = True + break + if any_visible and def_background: + for bg in backgrounds: + if bg['name'] == def_background['name']: + bg['visibility'] = True + break + else: + backgrounds = copy.deepcopy(defaults) + except Exception: + # traceback.print_exc() + backgrounds = copy.copy(defaults) + tb = traceback.format_exc() + logger.debug(tb) + return backgrounds + + def get_overlays(self, viewer, request=None): + overlays = [] + selected = None + try: + viewer_obj = json.loads(viewer) + layers = viewer_obj['map']['layers'] + sources = viewer_obj['sources'] + + for layer in layers: + if 'group' not in layer or layer['group'] != "background": + source = sources[layer['source']] + overlay = {} + if 'url' in source: + overlay['type'] = "wms" if 'ptype' not in source or \ + source['ptype'] != 'gxp_arcrestsource' else 'arcgis' + _p_url = parse.urlparse(source['url']) + if _p_url.query: + overlay['params'] = dict(parse.parse_qsl(_p_url.query)) + overlay['url'] = source['url'] + overlay['visibility'] = layer['visibility'] if 'visibility' in layer else True + overlay['singleTile'] = layer['singleTile'] if 'singleTile' in layer else False + overlay['selected'] = layer['selected'] if 'selected' in layer else False + overlay['hidden'] = layer['hidden'] if 'hidden' in layer else False + overlay['handleClickOnLayer'] = layer['handleClickOnLayer'] if \ + 'handleClickOnLayer' in layer else False + overlay['wrapDateLine'] = layer['wrapDateLine'] if 'wrapDateLine' in layer else False + overlay['hideLoading'] = layer['hideLoading'] if 'hideLoading' in layer else False + overlay['useForElevation'] = layer['useForElevation'] if 'useForElevation' in layer else False + overlay['fixed'] = layer['fixed'] if 'fixed' in layer else False + overlay['opacity'] = layer['opacity'] if 'opacity' in layer else 1.0 + overlay['title'] = layer['title'] if 'title' in layer else '' + overlay['name'] = layer['name'] if 'name' in layer else '' + overlay['store'] = layer['store'] if 'store' in layer else '' + overlay['group'] = layer['group'] if 'group' in layer else '' + overlay['format'] = layer['format'] if 'format' in layer else "image/png" + overlay['bbox'] = {} + + if 'dimensions' in layer: + overlay['dimensions'] = layer['dimensions'] + + if 'search' in layer: + overlay['search'] = layer['search'] + + if 'style' in layer: + overlay['style'] = layer['style'] + + if 'styles' in layer: + overlay['styles'] = layer['styles'] + + if 'layerFilter' in layer: + overlay['layerFilter'] = layer['layerFilter'] + + if 'capability' in layer: + capa = layer['capability'] + if 'store' in capa: + overlay['store'] = capa['store'] + if 'styles' in capa: + overlay['styles'] = capa['styles'] + if 'style' in capa: + overlay['style'] = capa['style'] + if 'abstract' in capa: + overlay['abstract'] = capa['abstract'] + if 'attribution' in capa: + overlay['attribution'] = capa['attribution'] + if 'keywords' in capa: + overlay['keywords'] = capa['keywords'] + if 'dimensions' in capa and capa['dimensions']: + overlay['dimensions'] = self.get_layer_dimensions(dimensions=capa['dimensions']) + if 'storeType' in capa and capa['storeType'] == 'dataStore': + overlay['search'] = { + "url": get_wfs_endpoint(request), + "type": "wfs" + } + if 'llbbox' in capa: + bbox = capa['llbbox'] + # Must be in the form xmin, ymin, xmax, ymax + llbbox = [ + get_valid_number(bbox[0]), + get_valid_number(bbox[2]), + get_valid_number(bbox[1]), + get_valid_number(bbox[3]), + ] + overlay['llbbox'] = llbbox + overlay['bbox']['bounds'] = { + "minx": llbbox[0], + "miny": llbbox[1], + "maxx": llbbox[2], + "maxy": llbbox[3] + } + overlay['bbox']['crs'] = 'EPSG:4326' + elif 'bbox' in capa: + bbox = capa['bbox'] + if viewer_obj['map']['projection'] in bbox: + proj = viewer_obj['map']['projection'] + bbox = capa['bbox'][proj] + overlay['bbox']['bounds'] = { + "minx": get_valid_number(bbox['bbox'][0]), + "miny": get_valid_number(bbox['bbox'][1]), + "maxx": get_valid_number(bbox['bbox'][2]), + "maxy": get_valid_number(bbox['bbox'][3]) + } + overlay['bbox']['crs'] = bbox['srs'] + + if 'nativeCrs' in layer: + overlay['nativeCrs'] = layer['nativeCrs'] + else: + try: + from geonode.layers.models import Layer + _gn_layer = Layer.objects.get( + store=overlay['store'], + alternate=overlay['name']) + if _gn_layer.srid: + overlay['nativeCrs'] = _gn_layer.srid + except Exception: + tb = traceback.format_exc() + logger.debug(tb) + + if 'bbox' in layer and not overlay['bbox']: + if 'bounds' in layer['bbox']: + overlay['bbox'] = layer['bbox'] + else: + overlay['bbox']['bounds'] = { + "minx": get_valid_number(layer['bbox'][0], + default=layer['bbox'][2], + complementar=True), + "miny": get_valid_number(layer['bbox'][1], + default=layer['bbox'][3], + complementar=True), + "maxx": get_valid_number(layer['bbox'][2], + default=layer['bbox'][0], + complementar=True), + "maxy": get_valid_number(layer['bbox'][3], + default=layer['bbox'][1], + complementar=True) + } + overlay['bbox']['crs'] = layer['srs'] if 'srs' in layer else \ + viewer_obj['map']['projection'] + + if 'ftInfoTemplate' in layer and layer['ftInfoTemplate']: + featureInfo = {'format': 'TEMPLATE'} + featureInfo['template'] = layer['ftInfoTemplate'] + overlay['featureInfo'] = featureInfo + elif 'getFeatureInfo' in layer and layer['getFeatureInfo']: + if 'fields' in layer['getFeatureInfo'] and layer['getFeatureInfo']['fields'] and \ + 'propertyNames' in layer['getFeatureInfo'] and \ + layer['getFeatureInfo']['propertyNames']: + fields = layer['getFeatureInfo']['fields'] + propertyNames = layer['getFeatureInfo']['propertyNames'] + displayTypes = layer['getFeatureInfo']['displayTypes'] if 'displayTypes' in layer['getFeatureInfo'] else dict() + featureInfo = {'format': 'TEMPLATE'} + + _template = '
' + for _field in fields: + _label = propertyNames[_field] if propertyNames[_field] else _field + _template += '
' + + if _field in displayTypes and displayTypes[_field] == 'type_href': + _template += '
%s:
\ + ' % \ + (_label, _field, _field) + elif _field in displayTypes and displayTypes[_field] == 'type_image': + _template += '
\ + %s
' % \ + (_field, _field, _label, _label) + elif _field in displayTypes and 'type_video' in displayTypes[_field]: + if 'youtube' in displayTypes[_field]: + _template += '
\ +
' % \ + (_field) + else: + _type = "video/%s" % (displayTypes[_field][11:]) + _template += '
\ +
' % \ + (_field, _type) + elif _field in displayTypes and displayTypes[_field] == 'type_audio': + _template += '
\ +
' % \ + (_field) + elif _field in displayTypes and displayTypes[_field] == 'type_iframe': + _template += '
\ +
' % \ + (_field) + else: + _template += '
%s:
\ +
${properties.%s}
' % \ + (propertyNames[_field] if propertyNames[_field] else _field, _field) + + _template += '
' + _template += '
' + + featureInfo['template'] = _template + overlay['featureInfo'] = featureInfo + + # Push extraParams into GeoNode layerParams + if 'extraParams' in layer and layer['extraParams']: + overlay['extraParams'] = layer['extraParams'] + elif 'name' in layer and layer['name'] == 'Annotations': + overlay = layer + + # Restore the id of ms2 layer + if "extraParams" in layer and "msId" in layer["extraParams"]: + overlay["id"] = layer["extraParams"]["msId"] + overlays.append(overlay) + if not selected or ('selected' in layer and layer['selected']): + selected = overlay + except Exception: + tb = traceback.format_exc() + logger.debug(tb) + + return (overlays, selected) + + def get_layer_dimensions(self, dimensions): + url = getattr(settings, "GEOSERVER_PUBLIC_LOCATION", "") + if url.endswith('ows'): + url = url[:-3] + url += "gwc/service/wmts" + dim = [] + for attr, value in dimensions.items(): + if attr == "time": + nVal = {"name": attr, "source": {"type": "multidim-extension", "url": url}} + dim.append(nVal) + else: + value["name"] = attr + dim.append(value) + return dim + + def get_center_and_zoom(self, view_map, overlay): + center = { + "x": get_valid_number( + overlay['bbox']['bounds']['minx'] + ( + overlay['bbox']['bounds']['maxx'] - overlay['bbox']['bounds']['minx'] + ) / 2), + "y": get_valid_number( + overlay['bbox']['bounds']['miny'] + ( + overlay['bbox']['bounds']['maxy'] - overlay['bbox']['bounds']['miny'] + ) / 2), + "crs": overlay['bbox']['crs'] + } + zoom = view_map['zoom'] + # max_extent = view_map['maxExtent'] + # map_crs = view_map['projection'] + ov_bbox = [get_valid_number(overlay['bbox']['bounds']['minx']), + get_valid_number(overlay['bbox']['bounds']['miny']), + get_valid_number(overlay['bbox']['bounds']['maxx']), + get_valid_number(overlay['bbox']['bounds']['maxy']), ] + ov_crs = overlay['bbox']['crs'] + (center_m, zoom_m) = self.project_to_WGS84(ov_bbox, ov_crs, center=None) + if center_m is not None and zoom_m is not None: + zoom_m = zoom_m if zoom_m > 0 else 1 + return (center_m, zoom_m) + else: + return (center, zoom) + + def project_to_WGS84(self, ov_bbox, ov_crs, center=None): + try: + srid = int(ov_crs.split(':')[1]) + srid = 3857 if srid == 900913 else srid + poly = Polygon(( + (ov_bbox[0], ov_bbox[1]), + (ov_bbox[0], ov_bbox[3]), + (ov_bbox[2], ov_bbox[3]), + (ov_bbox[2], ov_bbox[1]), + (ov_bbox[0], ov_bbox[1])), srid=srid) + if srid != 4326: + gcoord = SpatialReference(4326) + ycoord = SpatialReference(srid) + trans = CoordTransform(ycoord, gcoord) + poly.transform(trans) + try: + if not center: + center = { + "x": get_valid_number(poly.centroid.coords[0]), + "y": get_valid_number(poly.centroid.coords[1]), + "crs": "EPSG:4326" + } + zoom = GoogleZoom().get_zoom(poly) + 1 + except Exception: + center = (0, 0) + zoom = 0 + tb = traceback.format_exc() + logger.debug(tb) + except Exception: + tb = traceback.format_exc() + logger.debug(tb) + + return (center, zoom) + + def viewer_json(self, viewer, request): + """ + input: MapStore2 compliant str(config) + output: GeoNode JSON Gxp Config + """ + # MapStore uses x0,y0,x1,y1 ordering of bbox coords + viewer = to_json(viewer) + if viewer.get('map', None) and viewer['map'].get('bbox', None): + ms2_bbox = viewer['map'].get('bbox') + config_bbox = [ms2_bbox[0], ms2_bbox[2], ms2_bbox[1], ms2_bbox[3]] + viewer['map']['bbox'] = config_bbox + return viewer diff --git a/mapstore2_adapter/plugins/serializers.py b/mapstore2_adapter/plugins/serializers.py new file mode 100644 index 0000000000..123c30033d --- /dev/null +++ b/mapstore2_adapter/plugins/serializers.py @@ -0,0 +1,362 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +from __future__ import absolute_import + +from ..api.models import (MapStoreData, + MapStoreAttribute) + +from rest_framework.exceptions import APIException + +import json +import base64 +import logging +import traceback +from django.http import Http404 +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs +from geonode.layers.models import Layer +from geonode.base.bbox_utils import BBOXHelper + +is_analytics_enabled = False +try: + from geonode.monitoring.models import EventType + from geonode.monitoring import register_event + is_analytics_enabled = True +except ImportError: + pass + + +logger = logging.getLogger(__name__) + + +class GeoNodeSerializer(object): + + @classmethod + def update_data(cls, serializer, data): + if data: + _data, created = MapStoreData.objects.get_or_create( + resource=serializer.instance) + _data.resource = serializer.instance + _data.blob = data + _data.save() + serializer.validated_data['data'] = _data + + @classmethod + def update_attributes(cls, serializer, attributes): + _attributes = [] + for _a in attributes: + attribute, created = MapStoreAttribute.objects.get_or_create( + name=_a['name'], + resource=serializer.instance) + attribute.resource = serializer.instance + attribute.name = _a['name'] + attribute.type = _a['type'] + attribute.label = _a['label'] + if 'value' in _a: + attribute.value = base64.b64encode(_a['value'].encode('utf8')) + attribute.save() + _attributes.append(attribute) + serializer.validated_data['attributes'] = _attributes + + def get_queryset(self, caller, queryset): + allowed_map_ids = [] + for _q in queryset: + mapid = _q.id + try: + from geonode.maps.views import (_resolve_map, + _PERMISSION_MSG_VIEW) + map_obj = _resolve_map( + caller.request, + str(mapid), + 'base.view_resourcebase', + _PERMISSION_MSG_VIEW) + if map_obj: + allowed_map_ids.append(mapid) + except Exception: + tb = traceback.format_exc() + logger.debug(tb) + + queryset = queryset.filter(id__in=allowed_map_ids) + return queryset + + def get_geonode_map(self, caller, serializer): + from geonode.maps.views import _PERMISSION_MSG_SAVE + try: + from geonode.maps.views import _resolve_map + if 'id' in serializer.validated_data: + mapid = serializer.validated_data['id'] + map_obj = _resolve_map( + caller.request, + str(mapid), + 'base.change_resourcebase', + _PERMISSION_MSG_SAVE) + return map_obj + except Exception: + tb = traceback.format_exc() + logger.debug(tb) + raise APIException(_PERMISSION_MSG_SAVE) + + def set_geonode_map(self, caller, serializer, map_obj=None, data=None, attributes=None): + + def decode_base64(data): + """Decode base64, padding being optional. + + :param data: Base64 data as an ASCII byte string + :returns: The decoded byte string. + + """ + _thumbnail_format = 'png' + _invalid_padding = data.find(';base64,') + if _invalid_padding: + _thumbnail_format = data[data.find('image/') + len('image/'):_invalid_padding] + data = data[_invalid_padding + len(';base64,'):] + missing_padding = len(data) % 4 + if missing_padding != 0: + data += b'=' * (4 - missing_padding) + return (base64.b64decode(data), _thumbnail_format) + + _map_name = None + _map_title = None + _map_abstract = None + _map_thumbnail = None + _map_thumbnail_format = 'png' + if attributes: + for _a in attributes: + if _a['name'] == 'name' and 'value' in _a: + _map_name = _a['value'] + if _a['name'] == 'title' and 'value' in _a: + _map_title = _a['value'] + if _a['name'] == 'abstract' and 'value' in _a: + _map_abstract = _a['value'] + if 'thumb' in _a['name'] and 'value' in _a: + try: + (_map_thumbnail, _map_thumbnail_format) = decode_base64(_a['value']) + except Exception: + if _a['value']: + _map_thumbnail = _a['value'] + _map_thumbnail_format = 'link' + elif map_obj: + _map_title = map_obj.title + _map_abstract = map_obj.abstract + + _map_name = _map_name or None + if not _map_name and 'name' in serializer.validated_data: + _map_name = serializer.validated_data['name'] + _map_title = _map_title or _map_name + _map_abstract = _map_abstract or "" + if data: + try: + _map_conf = dict(data) + _map_conf["about"] = { + "name": _map_name, + "title": _map_title, + "abstract": _map_abstract} + _map_conf['sources'] = {} + from geonode.layers.views import layer_detail + _map_obj = data.pop('map', None) + if _map_obj: + _map_bbox = [] + for _lyr in _map_obj['layers']: + _lyr_context = {} + _lyr_store = _lyr['store'] if 'store' in _lyr else None + if not _lyr_store: + try: + _url = urlparse(_lyr['catalogURL']) + _lyr_store = Layer.objects.get( + uuid=parse_qs(_url.query)['id'][0]).store + except Exception: + try: + _lyr_store = Layer.objects.get( + alternate=_lyr['name'], + remote_service__base_url=_lyr['url']).store + except Exception: + _lyr_store = None + + _lyr_name = "%s:%s" % (_lyr_store, _lyr['name']) if _lyr_store else _lyr['name'] + try: + # Retrieve the Layer Params back from GeoNode + _gn_layer = layer_detail( + caller.request, + _lyr_name) + if _gn_layer and _gn_layer.context_data: + _context_data = json.loads(_gn_layer.context_data['viewer']) + for _gn_layer_ctx in _context_data['map']['layers']: + if 'name' in _gn_layer_ctx and _gn_layer_ctx['name'] == _lyr['name']: + _lyr['store'] = _lyr_store + if 'style' in _lyr: + _lyr_context['style'] = _lyr['style'] + _lyr_context = _gn_layer_ctx + _src_idx = _lyr_context['source'] + _map_conf['sources'][_src_idx] = _context_data['sources'][_src_idx] + except Http404: + tb = traceback.format_exc() + logger.debug(tb) + except Exception: + raise + # Store ms2 layer idq + if "id" in _lyr and _lyr["id"]: + _lyr['extraParams'] = {"msId": _lyr["id"]} + + # Store the Capabilities Document into the Layer Params of GeoNode + if _lyr_context: + if 'ftInfoTemplate' in _lyr_context: + _lyr['ftInfoTemplate'] = _lyr_context['ftInfoTemplate'] + if 'getFeatureInfo' in _lyr_context: + _lyr['getFeatureInfo'] = _lyr_context['getFeatureInfo'] + if 'capability' in _lyr_context: + _lyr['capability'] = _lyr_context['capability'] + if 'bbox' in _lyr_context['capability']: + _lyr_bbox = _lyr_context['capability']['bbox'] + if _map_obj['projection'] in _lyr_bbox: + x0 = _lyr_bbox[_map_obj['projection']]['bbox'][0] + x1 = _lyr_bbox[_map_obj['projection']]['bbox'][2] + y0 = _lyr_bbox[_map_obj['projection']]['bbox'][1] + y1 = _lyr_bbox[_map_obj['projection']]['bbox'][3] + + if len(_map_bbox) == 0: + _map_bbox = [x0, x1, y0, y1] + else: + from geonode.utils import bbox_to_wkt + from django.contrib.gis.geos import GEOSGeometry + + _l_wkt = bbox_to_wkt(x0, x1, y0, y1, + srid=_map_obj['projection']) + _m_wkt = bbox_to_wkt(_map_bbox[0], _map_bbox[2], + _map_bbox[1], _map_bbox[3], + srid=_map_obj['projection']) + _map_srid = int(_map_obj['projection'][5:]) + _l_poly = GEOSGeometry(_l_wkt, srid=_map_srid) + _m_poly = GEOSGeometry(_m_wkt, srid=_map_srid).union(_l_poly) + _map_bbox = _m_poly.extent + + if 'source' in _lyr_context: + _source = _map_conf['sources'][_lyr_context['source']] + if 'remote' in _source and _source['remote'] is True: + _lyr['source'] = _lyr_context['source'] + elif 'source' in _lyr: + _map_conf['sources'][_lyr['source']] = {} + event_type = None + if is_analytics_enabled: + event_type = EventType.EVENT_CHANGE + + if not map_obj: + # Update Map BBox + if 'bbox' not in _map_obj and (not _map_bbox or len(_map_bbox) != 4): + _map_bbox = _map_obj['maxExtent'] + # Must be in the form : [x0, x1, y0, y1] + _map_obj['bbox'] = [_map_bbox[0], _map_bbox[1], + _map_bbox[2], _map_bbox[3]] + # Create a new GeoNode Map + from geonode.maps.models import Map + map_obj = Map( + title=_map_title, + owner=caller.request.user, + center_x=_map_obj['center']['x'], + center_y=_map_obj['center']['y'], + projection=_map_obj['projection'], + zoom=_map_obj['zoom'], + srid=_map_obj['projection']) + if 'bbox' in _map_obj: + if hasattr(map_obj, 'bbox_polygon'): + map_obj.bbox_polygon = BBOXHelper.from_xy(_map_obj['bbox']).as_polygon() + else: + map_obj.bbox_x0 = _map_obj['bbox'][0] + map_obj.bbox_y0 = _map_obj['bbox'][1] + map_obj.bbox_x1 = _map_obj['bbox'][2] + map_obj.bbox_y1 = _map_obj['bbox'][3] + map_obj.save() + + if is_analytics_enabled: + event_type = EventType.EVENT_CREATE + + # Update GeoNode Map + _map_conf['map'] = _map_obj + map_obj.update_from_viewer( + _map_conf, + context={'config': _map_conf}) + + if is_analytics_enabled: + register_event(caller.request, event_type, map_obj) + + # Dumps thumbnail from MapStore2 Interface + if _map_thumbnail: + if _map_thumbnail_format == 'link': + map_obj.thumbnail_url = _map_thumbnail + else: + _map_thumbnail_filename = "map-%s-thumb.%s" % (map_obj.uuid, _map_thumbnail_format) + map_obj.save_thumbnail(_map_thumbnail_filename, _map_thumbnail) + + serializer.validated_data['id'] = map_obj.id + serializer.save(user=caller.request.user) + except Exception as e: + tb = traceback.format_exc() + logger.error(tb) + raise APIException(e) + else: + raise APIException("Map Configuration (data) is Mandatory!") + + def perform_create(self, caller, serializer): + _data = None + _attributes = None + + try: + _data = serializer.validated_data['data'].copy() + serializer.validated_data.pop('data') + except Exception as e: + logger.exception(e) + raise APIException("Map Configuration (data) is Mandatory!") + + try: + _attributes = serializer.validated_data['attributes'].copy() + serializer.validated_data.pop('attributes') + except Exception as e: + logger.exception(e) + raise APIException("Map Metadata (attributes) are Mandatory!") + + map_obj = self.get_geonode_map(caller, serializer) + self.set_geonode_map(caller, serializer, map_obj, _data.copy(), _attributes.copy()) + + if _data: + # Save JSON blob + GeoNodeSerializer.update_data(serializer, _data.copy()) + + if _attributes: + # Sabe Attributes + GeoNodeSerializer.update_attributes(serializer, _attributes.copy()) + + return serializer.save() + + def perform_update(self, caller, serializer): + map_obj = self.get_geonode_map(caller, serializer) + + _data = None + _attributes = None + + if 'data' in serializer.validated_data: + _data = serializer.validated_data['data'].copy() + serializer.validated_data.pop('data') + + # Save JSON blob + GeoNodeSerializer.update_data(serializer, _data.copy()) + + if 'attributes' in serializer.validated_data: + _attributes = serializer.validated_data['attributes'].copy() + serializer.validated_data.pop('attributes') + + # Sabe Attributes + GeoNodeSerializer.update_attributes(serializer, _attributes.copy()) + + self.set_geonode_map(caller, serializer, map_obj, _data, _attributes) + + return serializer.save() diff --git a/mapstore2_adapter/settings.py b/mapstore2_adapter/settings.py new file mode 100644 index 0000000000..d095a35aa8 --- /dev/null +++ b/mapstore2_adapter/settings.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### +from django.conf import settings + +try: + settings.TEMPLATES[0]['OPTIONS']['context_processors'] += [ + 'mapstore2_adapter.context_processors.resource_urls', ] +except Exception: + pass + +try: + settings.LOGGING["loggers"]["mapstore2_adapter"] = { + "handlers": ["console"], "level": "INFO", } +except Exception: + pass + +settings.MAPSTORE2_ADAPTER_SERIALIZER = "mapstore2_adapter.plugins.serializers.GeoNodeSerializer" + +MAP_BASELAYERS = getattr(settings, "MAPSTORE_BASELAYERS", []) +CATALOGUE_SERVICES = getattr(settings, "MAPSTORE_CATALOGUE_SERVICES", {}) +CATALOGUE_SELECTED_SERVICE = getattr(settings, "MAPSTORE_CATALOGUE_SELECTED_SERVICE", None) diff --git a/mapstore2_adapter/urls.py b/mapstore2_adapter/urls.py new file mode 100644 index 0000000000..ca0a4137f4 --- /dev/null +++ b/mapstore2_adapter/urls.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### +from django.conf.urls import url, include + +urlpatterns = [ + url(r'^', include('mapstore2_adapter.api.urls')), +] diff --git a/mapstore2_adapter/utils.py b/mapstore2_adapter/utils.py new file mode 100644 index 0000000000..ba4db6da94 --- /dev/null +++ b/mapstore2_adapter/utils.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### + +from __future__ import unicode_literals + +from math import atan, exp, log, pi, sin, isnan, isinf +try: + import json +except ImportError: + from django.utils import simplejson as json + +from urllib.parse import urljoin + +from mapstore2_adapter import DjangoMapstore2AdapterBaseException + +from django.conf import settings +from django.utils.six.moves import range +try: + from django.urls import reverse +except Exception: + # Django 2.0 + from django.urls import reverse +from django.contrib.gis.geos import GEOSGeometry, LinearRing, Point, Polygon + +# Constants used for degree to radian conversion, and vice-versa. +DTOR = pi / 180. +RTOD = 180. / pi + + +class GoogleZoom(object): + """ + GoogleZoom is a utility for performing operations related to the zoom + levels on Google Maps. + + This class is inspired by the OpenStreetMap Mapnik tile generation routine + `generate_tiles.py`, and the article "How Big Is the World" (Hack #16) in + "Google Maps Hacks" by Rich Gibson and Schuyler Erle. + + `generate_tiles.py` may be found at: + http://trac.openstreetmap.org/browser/applications/rendering/mapnik/generate_tiles.py + + "Google Maps Hacks" may be found at http://safari.oreilly.com/0596101619 + """ + + def __init__(self, num_zoom=19, tilesize=256): + "Initializes the Google Zoom object." + # Google's tilesize is 256x256, square tiles are assumed. + self._tilesize = tilesize + + # The number of zoom levels + self._nzoom = num_zoom + + # Initializing arrays to hold the parameters for each one of the + # zoom levels. + self._degpp = [] # Degrees per pixel + self._radpp = [] # Radians per pixel + self._npix = [] # 1/2 the number of pixels for a tile at the given zoom level + + # Incrementing through the zoom levels and populating the parameter arrays. + z = tilesize # The number of pixels per zoom level. + for i in range(num_zoom): + # Getting the degrees and radians per pixel, and the 1/2 the number of + # for every zoom level. + self._degpp.append(z / 360.) # degrees per pixel + self._radpp.append(z / (2 * pi)) # radians per pixel + self._npix.append(z / 2) # number of pixels to center of tile + + # Multiplying `z` by 2 for the next iteration. + z *= 2 + + def __len__(self): + "Returns the number of zoom levels." + return self._nzoom + + def get_lon_lat(self, lonlat): + "Unpacks longitude, latitude from GEOS Points and 2-tuples." + if isinstance(lonlat, Point): + lon, lat = lonlat.coords + else: + lon, lat = lonlat + return lon, lat + + def lonlat_to_pixel(self, lonlat, zoom): + "Converts a longitude, latitude coordinate pair for the given zoom level." + # Setting up, unpacking the longitude, latitude values and getting the + # number of pixels for the given zoom level. + lon, lat = self.get_lon_lat(lonlat) + npix = self._npix[zoom] + + # Calculating the pixel x coordinate by multiplying the longitude value + # with the number of degrees/pixel at the given zoom level. + px_x = round(npix + (lon * self._degpp[zoom])) + + # Creating the factor, and ensuring that 1 or -1 is not passed in as the + # base to the logarithm. Here's why: + # if fac = -1, we'll get log(0) which is undefined; + # if fac = 1, our logarithm base will be divided by 0, also undefined. + fac = min(max(sin(DTOR * lat), -0.9999), 0.9999) + + # Calculating the pixel y coordinate. + px_y = round(npix + (0.5 * log((1 + fac) / (1 - fac)) * (-1.0 * self._radpp[zoom]))) + + # Returning the pixel x, y to the caller of the function. + return (px_x, px_y) + + def pixel_to_lonlat(self, px, zoom): + "Converts a pixel to a longitude, latitude pair at the given zoom level." + if len(px) != 2: + raise TypeError('Pixel should be a sequence of two elements.') + + # Getting the number of pixels for the given zoom level. + npix = self._npix[zoom] + + # Calculating the longitude value, using the degrees per pixel. + lon = (px[0] - npix) / self._degpp[zoom] + + # Calculating the latitude value. + lat = RTOD * (2 * atan(exp((px[1] - npix) / (-1.0 * self._radpp[zoom]))) - 0.5 * pi) + + # Returning the longitude, latitude coordinate pair. + return (lon, lat) + + def tile(self, lonlat, zoom): + """ + Returns a Polygon corresponding to the region represented by a fictional + Google Tile for the given longitude/latitude pair and zoom level. This + tile is used to determine the size of a tile at the given point. + """ + # The given lonlat is the center of the tile. + delta = self._tilesize / 2 + + # Getting the pixel coordinates corresponding to the + # the longitude/latitude. + px = self.lonlat_to_pixel(lonlat, zoom) + + # Getting the lower-left and upper-right lat/lon coordinates + # for the bounding box of the tile. + ll = self.pixel_to_lonlat((px[0] - delta, px[1] - delta), zoom) + ur = self.pixel_to_lonlat((px[0] + delta, px[1] + delta), zoom) + + # Constructing the Polygon, representing the tile and returning. + return Polygon(LinearRing(ll, (ll[0], ur[1]), ur, (ur[0], ll[1]), ll), srid=4326) + + def get_bounds_zoom(self, bounds, srid=4326): + "Returns the optimal Zoom level for the given geometry." + geom = Polygon(( + (bounds[0], bounds[1]), + (bounds[0], bounds[3]), + (bounds[2], bounds[3]), + (bounds[2], bounds[1]), + (bounds[0], bounds[1])), srid=srid) + return self.get_zoom(geom) + + def get_zoom(self, geom): + "Returns the optimal Zoom level for the given geometry." + # Checking the input type. + if not isinstance(geom, GEOSGeometry) or geom.srid != 4326: + raise TypeError('get_zoom() expects a GEOS Geometry with an SRID of 4326.') + + # Getting the envelope for the geometry, and its associated width, height + # and centroid. + env = geom.envelope + env_w, env_h = self.get_width_height(env.extent) + center = env.centroid + + for z in range(self._nzoom): + # Getting the tile at the zoom level. + tile_w, tile_h = self.get_width_height(self.tile(center, z).extent) + + # When we span more than one tile, this is an approximately good + # zoom level. + if (env_w > tile_w) or (env_h > tile_h): + if z == 0: + raise DjangoMapstore2AdapterBaseException( + 'Geometry width and height should not exceed that of the Earth.') + return z - 1 + + # Otherwise, we've zoomed in to the max. + return self._nzoom - 1 + + def get_width_height(self, extent): + """ + Returns the width and height for the given extent. + """ + # Getting the lower-left, upper-left, and upper-right + # coordinates from the extent. + ll = Point(extent[:2]) + ul = Point(extent[0], extent[3]) + ur = Point(extent[2:]) + # Calculating the width and height. + height = ll.distance(ul) + width = ul.distance(ur) + return width, height + + +def get_wfs_endpoint(request): + try: + if request and request.user and request.user.is_authenticated: + wfs_url = urljoin(settings.SITEURL, reverse('ows_endpoint')) + else: + wfs_url = urljoin(settings.SITEURL, reverse('ows_endpoint')) + except Exception: + wfs_url = urljoin(settings.SITEURL, reverse('ows_endpoint')) + return wfs_url + + +def get_valid_number(number, default=None, complementar=False): + try: + x = float(number) + except Exception: + x = float('nan') + is_nan = isnan(x) + is_inf = isinf(x) + if not is_nan and not is_inf: + return x + elif default: + return default if not complementar else -default + return 0 + + +def to_json(config): + try: + return json.loads(config) + except Exception: + try: + return json.loads(config.decode()) + except Exception: + return config diff --git a/package.json b/package.json index 9e6ff36c7f..b26368d921 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geonode-mapstore-client", - "version": "1.0.0", + "version": "2.1.0", "description": "", "scripts": {}, "author": "GeoSolutions", diff --git a/requirements.txt b/requirements.txt index d5fd0e9e78..0bd2e05101 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,8 @@ -# -e git+https://github.com/GeoNode/django-mapstore-adapter.git@master#egg=django-mapstore-adapter -django-mapstore-adapter>=2.0.6 \ No newline at end of file +django>=2.2.0,<4.0 +idna>=2.5,<2.11 +requests>=2.13.0 +Markdown>=3.2.2 +MarkupSafe>=1.1.1 +jsonfield>=3.1.0 +djangorestframework>=3.8.2,<=3.12.2 +urllib3>=1.25.9 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..da351a158c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,45 @@ +[metadata] +name = django-geonode-mapstore-client +version = 2.1.0 +description = Use GeoNode client in your django projects +author = Alessio Fabiani +author_email = alessio.fabiani@geo-solutions.it +url = https://github.com/GeoNode/geonode-mapstore-client +download_url = https://github.com/GeoNode/geonode-mapstore-client/tarball/master +keywords = django, mapstore, mapstore2 +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Web Environment + Framework :: Django + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Topic :: Internet :: WWW/HTTP + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +packages = find: +include_package_data = True +zip_safe = False +install_requires = + django >= 2.2.0, < 3.0 + idna >= 2.5, < 2.11 + requests >= 2.13.0 + Markdown >= 2.6.11 + MarkupSafe >= 1.1.1 + djangorestframework >= 3.8.2, <= 3.12.2 + jsonfield >= 2.0.2 + urllib3 >= 1.25 + +[options.packages.find] +exclude = tests + +[bdist_wheel] +universal = 1 + +[flake8] +max-line-length = 120 +exclude=geonode_mapstore_client/*/migrations/*,geonode_mapstore_client/mapstore2_adapter/*/migrations/*,management,scripts,docs,static,migrations,geonode_mapstore_client/mapstore2_adapter/*settings.py +ignore=E121,E122,E124,E126,E226 diff --git a/setup.py b/setup.py index e6547398cd..2cdbfea158 100644 --- a/setup.py +++ b/setup.py @@ -1,45 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright 2018, GeoSolutions Sas. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +# +######################################################################### import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'VERSION')) as version_file: - version = version_file.read().strip() setup( - name='django-geonode-mapstore-client', - version=version, - author='Alessio Fabiani', - author_email='alessio.fabiani@gmail.com', - url='https://github.com/GeoNode/geonode-mapstore-client', - description="Use GeoNode client in your django projects", long_description=open(os.path.join(here, 'README.md')).read(), long_description_content_type='text/markdown', - license='BSD, see LICENSE file.', - install_requires=[ - "django-mapstore-adapter >= 2.0.4", - ], - - # adding packages - packages=find_packages(), - # trying to add files... - include_package_data = True, - package_data = { - '': ['*.*'], - '': ['static/*.*'], - 'static': ['*.*'], - '': ['templates/*.*'], - 'templates': ['*.*'], - }, - zip_safe = False, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Topic :: Internet :: WWW/HTTP', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - ], )