From 0bdaf4bf8251bb9e418558e475ef4b56c651950d Mon Sep 17 00:00:00 2001 From: Rizky Maulana Nugraha Date: Mon, 28 Aug 2017 16:40:28 +0700 Subject: [PATCH 1/8] QGIS Style support improve (#315) Add management command - Add import_qgis_style management command - Add unittest for import_qgis_style Improve Styles and Layers API - Wrap for Geoserver and QGIS Server backend - Add POST and DELETE method for Styles API - Add unittests for Styles API --- geonode/api/api.py | 334 +++++++++++------- geonode/api/resourcebase_api.py | 118 ++++++- geonode/api/tests.py | 225 ++++++++++++ geonode/api/urls.py | 8 +- geonode/base/populate_test_data.py | 20 +- geonode/layers/forms.py | 12 +- geonode/layers/models.py | 16 +- geonode/layers/views.py | 14 +- geonode/maps/urls.py | 7 +- .../management/commands/import_qgis_styles.py | 49 +++ .../qgis_server/management/commands/tests.py | 42 ++- .../migrations/0005_auto_20170823_0341.py | 20 ++ geonode/qgis_server/models.py | 36 +- geonode/qgis_server/tests/data/test_grid.qml | 16 + geonode/qgis_server/tests/test_views.py | 98 ++++- geonode/qgis_server/urls.py | 18 +- geonode/qgis_server/views.py | 24 +- geonode/static/geonode/js/upload/LayerInfo.js | 4 + geonode/upload/forms.py | 11 +- 19 files changed, 884 insertions(+), 188 deletions(-) create mode 100644 geonode/qgis_server/management/commands/import_qgis_styles.py create mode 100644 geonode/qgis_server/migrations/0005_auto_20170823_0341.py create mode 100644 geonode/qgis_server/tests/data/test_grid.qml diff --git a/geonode/api/api.py b/geonode/api/api.py index cb5a23bb510..2230197b7b9 100644 --- a/geonode/api/api.py +++ b/geonode/api/api.py @@ -27,11 +27,18 @@ from django.contrib.contenttypes.models import ContentType from django.conf import settings from django.db.models import Count +from django.http.response import HttpResponse +from django.template.response import TemplateResponse from django.utils.translation import get_language from avatar.templatetags.avatar_tags import avatar_url +from tastypie import http +from tastypie.exceptions import BadRequest + +from geonode import qgis_server, geoserver +from geonode.qgis_server.models import QGISServerStyle from guardian.shortcuts import get_objects_for_user -from tastypie.authorization import Authorization +from tastypie.authorization import DjangoAuthorization from tastypie.bundle import Bundle from geonode.base.models import ResourceBase @@ -40,7 +47,7 @@ from geonode.base.models import HierarchicalKeyword from geonode.base.models import ThesaurusKeywordLabel -from geonode.layers.models import Layer, Style, LayerFile +from geonode.layers.models import Layer, Style from geonode.maps.models import Map from geonode.documents.models import Document from geonode.groups.models import GroupProfile, GroupCategory @@ -48,10 +55,12 @@ from django.core.serializers.json import DjangoJSONEncoder from tastypie.serializers import Serializer from tastypie import fields -from tastypie.resources import ModelResource, Resource +from tastypie.resources import ModelResource from tastypie.constants import ALL, ALL_WITH_RELATIONS from tastypie.utils import trailing_slash +from geonode.utils import check_ogc_backend + FILTER_TYPES = { 'layer': Layer, 'map': Map, @@ -429,146 +438,219 @@ class Meta: serializer = CountJSONSerializer() -class GeoserverStyleResource(ModelResource): - """Styles api for Geoserver backend.""" +class QGISStyleResource(ModelResource): + """Styles API for QGIS Server backend.""" + + body = fields.CharField(attribute='body', use_in='detail') + name = fields.CharField(attribute='name') + title = fields.CharField(attribute='title') + layer = fields.ForeignKey( + 'geonode.api.resourcebase_api.LayerResource', + attribute='layer', + null=True) + style_url = fields.CharField(attribute='style_url') + type = fields.CharField(attribute='type') class Meta: - queryset = Style.objects.all() + queryset = QGISServerStyle.objects.all() resource_name = 'styles' - allowed_methods = ['get', 'post', 'put'] - ordering = ['layername'] + detail_uri_name = 'id' + authorization = DjangoAuthorization() + filtering = { + 'id': ALL, + 'title': ALL, + 'name': ALL, + 'layer': ALL_WITH_RELATIONS + } + + def populate_object(self, style): + """Populate results with necessary fields + + :param style: Style objects + :type style: QGISServerStyle + :return: + """ + try: + qgis_layer = style.layer_styles.first() + """:type: geonode.qgis_server.QGISServerLayer""" + style.layer = qgis_layer.layer + style.type = 'qml' + except: + pass + return style + + def build_filters(self, filters=None): + """Apply custom filters for layer.""" + filters = super(QGISStyleResource, self).build_filters(filters) + # Convert layer__ filters into layer_styles__layer__ + updated_filters = {} + for key, value in filters.iteritems(): + key = key.replace('layer__', 'layer_styles__layer__') + updated_filters[key] = value + return updated_filters + + def build_bundle( + self, obj=None, data=None, request=None, objects_saved=None): + """Override build_bundle method to add additional info.""" + + if obj is None and self._meta.object_class: + obj = self._meta.object_class() + + elif obj: + obj = self.populate_object(obj) + + return Bundle( + obj=obj, + data=data, + request=request, + objects_saved=objects_saved + ) + + def post_list(self, request, **kwargs): + """Attempt to redirect to QGIS Server Style management. + A post method should have the following field: -class QGISStyleObject(object): + name: Slug name of style + title: Title of style + style: the style file uploaded - def __init__(self, style_id=None, name=None, layer=None, - style_file=None, style_type='qml', style_url=None): - # File name of QML style file - self.name = name - # Related layers - if layer and len(layer) > 0: - self.layer = layer[0] - # File object of the style - self.file = style_file - # LayerFile id of QML style in upload session - self.id = style_id - # Extension type of this style - # Currently only for qml - self.type = style_type - # Url of downloadable QML Style - self.url = style_url + Also, should have kwargs: + layername or layer__name: The layer name associated with the style -class QGISStyleResource(Resource): - """Styles api for QGIS Server backend.""" + or + layer__id: The layer id associated with the style + + """ + from geonode.qgis_server.views import qml_style + + # Extract layer name information + POST = request.POST + FILES = request.FILES + layername = POST.get('layername') or POST.get('layer__name') + if not layername: + layer_id = POST.get('layer__id') + layer = Layer.objects.get(id=layer_id) + layername = layer.name + + # move style file + FILES['qml'] = FILES['style'] + + response = qml_style(request, layername) + + if isinstance(response, TemplateResponse): + if response.status_code == 201: + obj = QGISServerStyle.objects.get( + layer_styles__layer__name=layername, + name=POST['name']) + updated_bundle = self.build_bundle(obj=obj, request=request) + location = self.get_resource_uri(updated_bundle) + + if not self._meta.always_return_data: + return http.HttpCreated(location=location) + else: + updated_bundle = self.full_dehydrate(updated_bundle) + updated_bundle = self.alter_detail_data_to_serialize( + request, updated_bundle) + return self.create_response( + request, updated_bundle, + response_class=http.HttpCreated, + location=location) + else: + context = response.context_data + # Check form valid + style_upload_form = context['style_upload_form'] + if not style_upload_form.is_valid(): + raise BadRequest(style_upload_form.errors.as_text()) + alert_message = context['alert_message'] + raise BadRequest(alert_message) + elif isinstance(response, HttpResponse): + response_class = None + if response.status_code == 403: + response_class = http.HttpForbidden + return self.error_response( + request, response.content, + response_class=response_class) + + +class GeoserverStyleResource(ModelResource): + """Styles API for Geoserver backend.""" + body = fields.CharField( + attribute='sld_body', + use_in='detail') name = fields.CharField(attribute='name') - layer = fields.ForeignKey( + title = fields.CharField(attribute='sld_title') + # layer_default_style is polymorphic, so it will have many to many + # relation + layer = fields.ManyToManyField( 'geonode.api.resourcebase_api.LayerResource', - attribute='layer') - id = fields.IntegerField(attribute='id') + attribute='layer_default_style', + null=True) + version = fields.CharField( + attribute='sld_version', + null=True, + blank=True) + style_url = fields.CharField(attribute='sld_url') + workspace = fields.CharField(attribute='workspace', null=True) type = fields.CharField(attribute='type') - url = fields.CharField(attribute='url', null=True) - file = fields.FileField(attribute='file') class Meta: + queryset = Style.objects.all() resource_name = 'styles' detail_uri_name = 'id' - object_class = QGISStyleObject - authorization = Authorization() + authorization = DjangoAuthorization() + filtering = { + 'id': ALL, + 'title': ALL, + 'name': ALL, + 'layer': ALL_WITH_RELATIONS + } - def _build_style_url(self, layerfile): - """Build downloadable url for QML Style. + def build_filters(self, filters=None): + """Apply custom filters for layer.""" + filters = super(GeoserverStyleResource, self).build_filters(filters) + # Convert layer__ filters into layer_styles__layer__ + updated_filters = {} + for key, value in filters.iteritems(): + key = key.replace('layer__', 'layer_default_style__') + updated_filters[key] = value + return updated_filters + + def populate_object(self, style): + """Populate results with necessary fields + + :param style: Style objects + :type style: Style + :return: + """ + style.type = 'sld' + return style - :param layerfile: LayerFile object - :type layerfile: geonode.layers.models.LayerFile + def build_bundle( + self, obj=None, data=None, request=None, objects_saved=None): + """Override build_bundle method to add additional info.""" - :return: url - """ - try: - layer = Layer.objects.get(upload_session=layerfile.upload_session) - layername = layer.name - style_url = reverse( - 'qgis_server:download-qml', - kwargs={'layername': layername}) - except Layer.DoesNotExist: - # There exists a stale layerfile - return None - return style_url - - def _generate_result(self, layerfiles): - results = [ - QGISStyleObject( - layer=lf.upload_session.layer_set.all(), - style_id=lf.id, - name=lf.file.name, - style_file=lf.file, - style_url=self._build_style_url(lf)) for lf in layerfiles - ] - results = [r for r in results if r.url] - return results - - def detail_uri_kwargs(self, bundle_or_obj): - kwargs = {} - - if isinstance(bundle_or_obj, Bundle): - kwargs[self._meta.detail_uri_name] = getattr( - bundle_or_obj.obj, self._meta.detail_uri_name) - else: - kwargs[self._meta.detail_uri_name] = getattr( - bundle_or_obj, self._meta.detail_uri_name) - - return kwargs - - def obj_get_list(self, bundle, **kwargs): - styles = [] - if 'layername' in kwargs: - layername = kwargs.pop('layername') - layer = Layer.objects.get(name=layername) - styles = layer.upload_session.layerfile_set.filter(name='qml') - if not kwargs.items(): - styles = LayerFile.objects.filter(name='qml') - return self._generate_result(styles) - - def obj_get(self, bundle, **kwargs): - styles = [] - if self._meta.detail_uri_name in kwargs: - style_id = kwargs.pop(self._meta.detail_uri_name) - styles = LayerFile.objects.filter(id=style_id) - results = self._generate_result(styles) - return results[0] - - def obj_create(self, bundle, **kwargs): - bundle.obj = QGISStyleObject() - bundle = self.full_hydrate(bundle) - return bundle - - def obj_update(self, bundle, **kwargs): - return self.obj_create(bundle, **kwargs) - - def obj_delete_list(self, bundle, **kwargs): - bucket = self._bucket() - - for key in bucket.get_keys(): - obj = bucket.get(key) - obj.delete() - - def obj_delete(self, bundle, **kwargs): - bucket = self._bucket() - obj = bucket.get(kwargs['pk']) - obj.delete() - - def deserialize(self, request, data, format=None): - if not format: - format = request.Meta.get('CONTENT_TYPE', 'application/json') - if format == 'application/x-www-form-urlencoded': - return request.POST - if format.startswith('multipart'): - data = request.POST.copy() - data.update(request.FILES) - return data - return super(QGISStyleResource, self).deserialize( - request, data, format) - - def rollback(self, bundles): + if obj is None and self._meta.object_class: + obj = self._meta.object_class() + + elif obj: + obj = self.populate_object(obj) + + return Bundle( + obj=obj, + data=data, + request=request, + objects_saved=objects_saved + ) + + +if check_ogc_backend(qgis_server.BACKEND_PACKAGE): + class StyleResource(QGISStyleResource): + """Wrapper for Generic Style Resource""" + pass +elif check_ogc_backend(geoserver.BACKEND_PACKAGE): + class StyleResource(GeoserverStyleResource): + """Wrapper for Generic Style Resource""" pass diff --git a/geonode/api/resourcebase_api.py b/geonode/api/resourcebase_api.py index f6a720e1903..b3eadb7fc1e 100644 --- a/geonode/api/resourcebase_api.py +++ b/geonode/api/resourcebase_api.py @@ -22,6 +22,7 @@ from django.db.models import Q from django.http import HttpResponse from django.conf import settings +from tastypie.bundle import Bundle from tastypie.constants import ALL, ALL_WITH_RELATIONS from tastypie.resources import ModelResource @@ -38,12 +39,13 @@ from tastypie.utils.mime import build_content_type -from geonode import get_version +from geonode import get_version, qgis_server, geoserver from geonode.layers.models import Layer from geonode.maps.models import Map from geonode.documents.models import Document from geonode.base.models import ResourceBase from geonode.base.models import HierarchicalKeyword +from geonode.utils import check_ogc_backend from .authorization import GeoNodeAuthorization @@ -569,6 +571,30 @@ class Meta(CommonMetaApi): class LayerResource(CommonModelApi): """Layer API""" + links = fields.ListField( + attribute='links', + null=True, + default=[]) + if check_ogc_backend(qgis_server.BACKEND_PACKAGE): + default_style = fields.ForeignKey( + 'geonode.api.api.StyleResource', + attribute='qgis_default_style', + null=True) + styles = fields.ManyToManyField( + 'geonode.api.api.StyleResource', + attribute='qgis_styles', + null=True, + use_in='detail') + elif check_ogc_backend(geoserver.BACKEND_PACKAGE): + default_style = fields.ForeignKey( + 'geonode.api.api.StyleResource', + attribute='default_style', + null=True) + styles = fields.ManyToManyField( + 'geonode.api.api.StyleResource', + attribute='styles', + null=True, + use_in='detail') def format_objects(self, objects): """ @@ -585,31 +611,95 @@ def format_objects(self, objects): formatted_obj = model_to_dict(obj, fields=values) # add the geogig link formatted_obj['geogig_link'] = obj.geogig_link - # add Link link - link_fields = [ - 'extension', - 'link_type', - 'name', - 'mime', - 'url' - ] - links = [] - for l in obj.link_set.all(): - formatted_link = model_to_dict(l, fields=link_fields) - links.append(formatted_link) - formatted_obj['links'] = links + # provide style information + bundle = self.build_bundle(obj=obj) + formatted_obj['default_style'] = ( + self.default_style.dehydrate(bundle, for_list=True)) + # Add resource uri + formatted_obj['resource_uri'] = self.get_resource_uri(bundle) # put the object on the response stack formatted_objects.append(formatted_obj) return formatted_objects + def dehydrate_links(self, bundle): + """Dehydrate links field.""" + obj = bundle.obj + link_fields = [ + 'extension', + 'link_type', + 'name', + 'mime', + 'url' + ] + dehydrated = [] + for l in obj.link_set.all(): + formatted_link = model_to_dict(l, fields=link_fields) + dehydrated.append(formatted_link) + + return dehydrated + + def dehydrate(self, bundle): + """Override dehydrate phase""" + + # Override Link dehydrate phase + bundle.data[self.links.instance_name] = self.dehydrate_links(bundle) + + return bundle + + def populate_object(self, obj): + """Populate results with necessary fields + + :param obj: Layer obj + :type obj: Layer + :return: + """ + if check_ogc_backend(qgis_server.BACKEND_PACKAGE): + # Provides custom links for QGIS Server styles info + # Default style + try: + obj.qgis_default_style = obj.qgis_layer.default_style + except: + pass + + # Styles + try: + obj.qgis_styles = obj.qgis_layer.styles + except: + pass + return obj + + def build_bundle( + self, obj=None, data=None, request=None, objects_saved=None): + """Override build_bundle method to add additional info.""" + + if obj is None and self._meta.object_class: + obj = self._meta.object_class() + + elif obj: + obj = self.populate_object(obj) + + return Bundle( + obj=obj, + data=data, + request=request, + objects_saved=objects_saved) + class Meta(CommonMetaApi): queryset = Layer.objects.distinct().order_by('-date') if settings.RESOURCE_PUBLISHING: queryset = queryset.filter(is_published=True) resource_name = 'layers' detail_uri_name = 'id' + include_resource_uri = True excludes = ['csw_anytext', 'metadata_xml'] + filtering = CommonMetaApi.filtering + # Allow filtering using ID + filtering.update({ + 'id': ALL, + 'name': ALL, + 'typename': ALL, + }) class MapResource(CommonModelApi): diff --git a/geonode/api/tests.py b/geonode/api/tests.py index 6bd38fb561c..d8c5af32f23 100644 --- a/geonode/api/tests.py +++ b/geonode/api/tests.py @@ -17,12 +17,22 @@ # along with this program. If not, see . # ######################################################################### +import os +from StringIO import StringIO +import gisdata +from django.core.management import call_command from django.core.urlresolvers import reverse +from django.test.testcases import LiveServerTestCase + +from geonode import geoserver, qgis_server +from geonode.decorators import on_ogc_backend +from geonode.layers.utils import file_upload from tastypie.test import ResourceTestCase from geonode.base.populate_test_data import create_models, all_public from geonode.layers.models import Layer +from geonode.utils import check_ogc_backend class PermissionsApiTests(ResourceTestCase): @@ -226,3 +236,218 @@ def test_date_filter(self): resp = self.api_client.get(filter_url) self.assertValidJSONResponse(resp) self.assertEquals(len(self.deserialize(resp)['objects']), 4) + + +class LayersStylesApiInteractionTests(LiveServerTestCase, ResourceTestCase): + + """Test Layers""" + + fixtures = ['initial_data.json', 'bobby'] + + def setUp(self): + super(LayersStylesApiInteractionTests, self).setUp() + + call_command('loaddata', 'people_data', verbosity=0) + + self.layer_list_url = reverse( + 'api_dispatch_list', + kwargs={ + 'api_name': 'api', + 'resource_name': 'layers'}) + self.style_list_url = reverse( + 'api_dispatch_list', + kwargs={ + 'api_name': 'api', + 'resource_name': 'styles'}) + filename = os.path.join(gisdata.GOOD_DATA, 'raster/test_grid.tif') + self.layer = file_upload(filename) + all_public() + + def tearDown(self): + Layer.objects.all().delete() + + @on_ogc_backend(qgis_server.BACKEND_PACKAGE) + def test_layer_interaction(self): + """Layer API interaction check.""" + layer_id = self.layer.id + + layer_detail_url = reverse( + 'api_dispatch_detail', + kwargs={ + 'api_name': 'api', + 'resource_name': 'layers', + 'id': layer_id + } + ) + resp = self.api_client.get(layer_detail_url) + self.assertValidJSONResponse(resp) + obj = self.deserialize(resp) + # Should have links + self.assertTrue('links' in obj and obj['links']) + # Should have default style + self.assertTrue('default_style' in obj and obj['default_style']) + # Should have styles + self.assertTrue('styles' in obj and obj['styles']) + + # Test filter layers by id + filter_url = self.layer_list_url + '?id=' + str(layer_id) + resp = self.api_client.get(filter_url) + self.assertValidJSONResponse(resp) + # This is a list url + objects = self.deserialize(resp)['objects'] + self.assertEqual(len(objects), 1) + obj = objects[0] + # Should not have links (to save payload from big text) + self.assertTrue('links' not in obj) + # Should not have styles + self.assertTrue('styles' not in obj) + # Should have default_style + self.assertTrue('default_style' in obj and obj['default_style']) + # Should have resource_uri to browse layer detail + self.assertTrue('resource_uri' in obj and obj['resource_uri']) + + prev_obj = obj + # Test filter layers by name + filter_url = self.layer_list_url + '?name=' + self.layer.name + resp = self.api_client.get(filter_url) + self.assertValidJSONResponse(resp) + # This is a list url + objects = self.deserialize(resp)['objects'] + self.assertEqual(len(objects), 1) + obj = objects[0] + + self.assertEqual(obj, prev_obj) + + @on_ogc_backend(qgis_server.BACKEND_PACKAGE) + def test_style_interaction(self): + """Style API interaction check.""" + + # filter styles by layer id + filter_url = self.style_list_url + '?layer__id=' + str(self.layer.id) + resp = self.api_client.get(filter_url) + self.assertValidJSONResponse(resp) + # This is a list url + objects = self.deserialize(resp)['objects'] + + self.assertEqual(len(objects), 1) + + # filter styles by layer name + filter_url = self.style_list_url + '?layer__name=' + self.layer.name + resp = self.api_client.get(filter_url) + self.assertValidJSONResponse(resp) + # This is a list url + objects = self.deserialize(resp)['objects'] + + self.assertEqual(len(objects), 1) + + # Check necessary list fields + obj = objects[0] + field_list = [ + 'layer', + 'name', + 'title', + 'style_url', + 'type', + 'resource_uri' + ] + + # Additional field based on OGC Backend + if check_ogc_backend(geoserver.BACKEND_PACKAGE): + field_list += [ + 'version', + 'workspace' + ] + elif check_ogc_backend(qgis_server.BACKEND_PACKAGE): + field_list += [ + 'style_legend_url' + ] + for f in field_list: + self.assertTrue(f in obj) + + if check_ogc_backend(geoserver.BACKEND_PACKAGE): + self.assertEqual(obj['type'], 'sld') + elif check_ogc_backend(qgis_server.BACKEND_PACKAGE): + self.assertEqual(obj['type'], 'qml') + + # Check style detail + detail_url = obj['resource_uri'] + resp = self.api_client.get(detail_url) + self.assertValidJSONResponse(resp) + obj = self.deserialize(resp) + + # should include body field + self.assertTrue('body' in obj and obj['body']) + + @on_ogc_backend(qgis_server.BACKEND_PACKAGE) + def test_add_delete_styles(self): + """Style API Add/Delete interaction.""" + # Check styles count + style_list_url = reverse( + 'api_dispatch_list', + kwargs={ + 'api_name': 'api', + 'resource_name': 'styles' + } + ) + filter_url = style_list_url + '?layer__name=' + self.layer.name + resp = self.api_client.get(filter_url) + self.assertValidJSONResponse(resp) + objects = self.deserialize(resp)['objects'] + + self.assertEqual(len(objects), 1) + + # Fetch default style + layer_detail_url = reverse( + 'api_dispatch_detail', + kwargs={ + 'api_name': 'api', + 'resource_name': 'layers', + 'id': self.layer.id + } + ) + resp = self.api_client.get(layer_detail_url) + self.assertValidJSONResponse(resp) + obj = self.deserialize(resp) + # Take default style url from Layer detail info + default_style_url = obj['default_style'] + resp = self.api_client.get(default_style_url) + self.assertValidJSONResponse(resp) + obj = self.deserialize(resp) + style_body = obj['body'] + + style_stream = StringIO(style_body) + # Add virtual filename + style_stream.name = 'style.qml' + data = { + 'layer__id': self.layer.id, + 'name': 'new_style', + 'title': 'New Style', + 'style': style_stream + } + # Use default client to request + resp = self.client.post(style_list_url, data=data) + + # Should not be able to add style without authentication + self.assertEqual(resp.status_code, 403) + + # Login using anonymous user + self.client.login(username='AnonymousUser') + style_stream.seek(0) + resp = self.client.post(style_list_url, data=data) + # Should not be able to add style without correct permission + self.assertEqual(resp.status_code, 403) + self.client.logout() + + # Use admin credentials + self.client.login(username='admin', password='admin') + style_stream.seek(0) + resp = self.client.post(style_list_url, data=data) + self.assertEqual(resp.status_code, 201) + + # Check styles count + filter_url = style_list_url + '?layer__name=' + self.layer.name + resp = self.api_client.get(filter_url) + self.assertValidJSONResponse(resp) + objects = self.deserialize(resp)['objects'] + + self.assertEqual(len(objects), 2) diff --git a/geonode/api/urls.py b/geonode/api/urls.py index 074acec4695..f959d854c74 100644 --- a/geonode/api/urls.py +++ b/geonode/api/urls.py @@ -20,9 +20,7 @@ from tastypie.api import Api -from geonode import qgis_server -from geonode.api.api import QGISStyleResource -from geonode.utils import check_ogc_backend +from geonode.api.api import StyleResource from .api import TagResource, TopicCategoryResource, ProfileResource, \ GroupResource, RegionResource, OwnersResource, ThesaurusKeywordResource, \ GroupCategoryResource @@ -44,6 +42,4 @@ api.register(OwnersResource()) api.register(ThesaurusKeywordResource()) api.register(GroupCategoryResource()) - -if check_ogc_backend(qgis_server.BACKEND_PACKAGE): - api.register(QGISStyleResource()) +api.register(StyleResource()) diff --git a/geonode/base/populate_test_data.py b/geonode/base/populate_test_data.py index 89643383286..8ea302ae923 100644 --- a/geonode/base/populate_test_data.py +++ b/geonode/base/populate_test_data.py @@ -38,7 +38,8 @@ import os.path -if 'geonode.geoserver' in settings.INSTALLED_APPS: +def disconnect_signals(): + """Disconnect signals for test class purposes.""" from django.db.models import signals from geonode.geoserver.signals import geoserver_pre_save_maplayer from geonode.geoserver.signals import geoserver_post_save_map @@ -49,6 +50,23 @@ signals.pre_save.disconnect(geoserver_pre_save, sender=Layer) signals.post_save.disconnect(geoserver_post_save, sender=Layer) + +def reconnect_signals(): + """Reconnect signals for test class purposes.""" + from django.db.models import signals + from geonode.geoserver.signals import geoserver_pre_save_maplayer + from geonode.geoserver.signals import geoserver_post_save_map + from geonode.geoserver.signals import geoserver_pre_save + from geonode.geoserver.signals import geoserver_post_save + signals.pre_save.connect(geoserver_pre_save_maplayer, sender=MapLayer) + signals.post_save.connect(geoserver_post_save_map, sender=Map) + signals.pre_save.connect(geoserver_pre_save, sender=Layer) + signals.post_save.connect(geoserver_post_save, sender=Layer) + + +if 'geonode.geoserver' in settings.INSTALLED_APPS: + disconnect_signals() + # This is used to populate the database with the search fixture data. This is # primarily used as a first step to generate the json data for the fixture using # django's dumpdata diff --git a/geonode/layers/forms.py b/geonode/layers/forms.py index b7ae67e0c8f..c47c140c3ff 100644 --- a/geonode/layers/forms.py +++ b/geonode/layers/forms.py @@ -25,6 +25,10 @@ from django.conf import settings from django import forms + +from geonode import geoserver, qgis_server +from geonode.utils import check_ogc_backend + try: import json except ImportError: @@ -193,9 +197,9 @@ def write_files(self): class NewLayerUploadForm(LayerUploadForm): - if 'geonode.geoserver' in settings.INSTALLED_APPS: + if check_ogc_backend(geoserver.BACKEND_PACKAGE): sld_file = forms.FileField(required=False) - if 'geonode.qgis_server' in settings.INSTALLED_APPS: + if check_ogc_backend(qgis_server.BACKEND_PACKAGE): qml_file = forms.FileField(required=False) xml_file = forms.FileField(required=False) @@ -213,9 +217,9 @@ class NewLayerUploadForm(LayerUploadForm): "xml_file", ] # Adding style file based on the backend - if 'geonode.geoserver' in settings.INSTALLED_APPS: + if check_ogc_backend(geoserver.BACKEND_PACKAGE): spatial_files.append('sld_file') - if 'geonode.qgis_server' in settings.INSTALLED_APPS: + if check_ogc_backend(qgis_server.BACKEND_PACKAGE): spatial_files.append('qml_file') spatial_files = tuple(spatial_files) diff --git a/geonode/layers/models.py b/geonode/layers/models.py index 573b519c0e3..5127fb61af6 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -35,7 +35,9 @@ from geonode.people.utils import get_valid_user from agon_ratings.models import OverallRating from geonode.utils import check_shp_columnnames -from geonode.security.models import remove_object_permissions +from geonode.security.models import ( + remove_object_permissions, + PermissionLevelMixin) logger = logging.getLogger("geonode.layers.models") @@ -59,7 +61,7 @@ } -class Style(models.Model): +class Style(models.Model, PermissionLevelMixin): """Model for storing styles. """ @@ -89,6 +91,16 @@ def absolute_url(self): logger.error("SLD URL is empty for Style %s" % self.name.encode('utf-8')) return None + def get_self_resource(self): + """Get associated resource base.""" + # Associate this model with resource + try: + layer = self.layer_styles.first() + """:type: Layer""" + return layer.get_self_resource() + except: + return None + class LayerManager(ResourceBaseManager): diff --git a/geonode/layers/views.py b/geonode/layers/views.py index 324c3017168..2e8f70c3356 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -41,6 +41,9 @@ from django.conf import settings from django.template import RequestContext from django.utils.translation import ugettext as _ + +from geonode import geoserver, qgis_server + try: import json except ImportError: @@ -61,7 +64,7 @@ from geonode.base.models import TopicCategory from geonode.groups.models import GroupProfile -from geonode.utils import default_map_config +from geonode.utils import default_map_config, check_ogc_backend from geonode.utils import GXPLayer from geonode.utils import GXPMap from geonode.layers.utils import file_upload, is_raster, is_vector @@ -77,7 +80,7 @@ if 'geonode.geoserver' in settings.INSTALLED_APPS: from geonode.geoserver.helpers import _render_thumbnail -if 'geonode.qgis_server' in settings.INSTALLED_APPS: +if check_ogc_backend(qgis_server.BACKEND_PACKAGE): from geonode.qgis_server.models import QGISServerLayer CONTEXT_LOG_FILE = ogc_server_settings.LOG_FILE @@ -202,6 +205,7 @@ def layer_upload(request, template='upload/layer_upload.html'): out['url'] = reverse( 'layer_detail', args=[ saved_layer.service_typename]) + out['ogc_backend'] = settings.OGC_SERVER['default']['BACKEND'] upload_session = saved_layer.upload_session upload_session.processed = True upload_session.save() @@ -765,17 +769,19 @@ def layer_replace(request, layername, template='layers/layer_replace.html'): out['success'] = False out['errors'] = _("You are attempting to replace a raster layer with a vector.") else: - if 'geonode.geoserver' in settings.INSTALLED_APPS: + if check_ogc_backend(geoserver.BACKEND_PACKAGE): # delete geoserver's store before upload cat = gs_catalog cascading_delete(cat, layer.typename) - elif 'geonode.qgis_server' in settings.INSTALLED_APPS: + out['ogc_backend'] = geoserver.BACKEND_PACKAGE + elif check_ogc_backend(qgis_server.BACKEND_PACKAGE): try: qgis_layer = QGISServerLayer.objects.get( layer=layer) qgis_layer.delete() except QGISServerLayer.DoesNotExist: pass + out['ogc_backend'] = qgis_server.BACKEND_PACKAGE saved_layer = file_upload( base_file, diff --git a/geonode/maps/urls.py b/geonode/maps/urls.py index 58e55bf8016..0f4baafbac5 100644 --- a/geonode/maps/urls.py +++ b/geonode/maps/urls.py @@ -19,10 +19,11 @@ ######################################################################### from django.conf.urls import patterns, url -from django.conf import settings from django.views.generic import TemplateView +from geonode import geoserver, qgis_server from geonode.maps.qgis_server_views import MapCreateView, MapDetailView +from geonode.utils import check_ogc_backend js_info_dict = { 'packages': ('geonode.maps',), @@ -31,11 +32,11 @@ new_map_view = 'new_map' existing_map_view = 'map_view' -if 'geonode.geoserver' in settings.INSTALLED_APPS: +if check_ogc_backend(geoserver.BACKEND_PACKAGE): new_map_view = 'new_map' existing_map_view = 'map_view' -elif 'geonode.qgis_server' in settings.INSTALLED_APPS: +elif check_ogc_backend(qgis_server.BACKEND_PACKAGE): new_map_view = MapCreateView.as_view() existing_map_view = MapDetailView.as_view() diff --git a/geonode/qgis_server/management/commands/import_qgis_styles.py b/geonode/qgis_server/management/commands/import_qgis_styles.py new file mode 100644 index 00000000000..77c390767b2 --- /dev/null +++ b/geonode/qgis_server/management/commands/import_qgis_styles.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2016 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.core.management.base import BaseCommand +from geonode.layers.models import Layer +from geonode.qgis_server.helpers import style_list + + +class Command(BaseCommand): + help = ("Import QGIS Server styles associated with layers.") + + def handle(self, *args, **options): + layers = Layer.objects.all() + + for l in layers: + try: + l.qgis_layer + except: + print 'Layer %s has no associated qgis_layer' % l.name + continue + + if l.qgis_layer: + print 'Fetching styles for layer %s' % l.name + + try: + styles = style_list(l, internal=False) + except: + print 'Failed to fetch styles' + continue + + print 'Successfully fetch %d style(s)' % len(styles) + print '' diff --git a/geonode/qgis_server/management/commands/tests.py b/geonode/qgis_server/management/commands/tests.py index 0d4ddf59339..edf3c6f724f 100644 --- a/geonode/qgis_server/management/commands/tests.py +++ b/geonode/qgis_server/management/commands/tests.py @@ -24,7 +24,7 @@ import gisdata from django.core.management import call_command from django.test import LiveServerTestCase -from geonode.qgis_server.models import QGISServerLayer +from geonode.qgis_server.models import QGISServerLayer, QGISServerStyle from geonode.layers.models import Layer @@ -134,3 +134,43 @@ def test_import_layers(self): # Cleanup layer.delete() + + def test_import_qgis_styles(self): + """import_qgis_styles management commands should run properly.""" + filename = os.path.join( + gisdata.GOOD_DATA, + 'vector/san_andres_y_providencia_administrative.shp') + call_command('importlayers', filename, overwrite=True) + + # Check layer have default style after importing + layer = Layer.objects.get( + name='san_andres_y_providencia_administrative') + + qgis_layer = layer.qgis_layer + self.assertTrue(qgis_layer.default_style) + + self.assertTrue(QGISServerStyle.objects.count() == 1) + + # Delete styles + qgis_layer.default_style.delete() + + self.assertTrue(QGISServerStyle.objects.count() == 0) + + qgis_layer.refresh_from_db() + + self.assertFalse(qgis_layer.default_style) + + # Import styles + + call_command('import_qgis_styles') + + layer.refresh_from_db() + qgis_layer = layer.qgis_layer + qgis_layer.refresh_from_db() + + self.assertTrue(qgis_layer.default_style) + + self.assertTrue(QGISServerStyle.objects.count() == 1) + + # Cleanup + layer.delete() diff --git a/geonode/qgis_server/migrations/0005_auto_20170823_0341.py b/geonode/qgis_server/migrations/0005_auto_20170823_0341.py new file mode 100644 index 00000000000..63d413bf447 --- /dev/null +++ b/geonode/qgis_server/migrations/0005_auto_20170823_0341.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('qgis_server', '0004_auto_20170805_0223'), + ] + + operations = [ + migrations.AlterField( + model_name='qgisserverlayer', + name='default_style', + field=models.ForeignKey(related_name='layer_default_style', on_delete=django.db.models.deletion.SET_NULL, default=None, to='qgis_server.QGISServerStyle', null=True), + ), + ] diff --git a/geonode/qgis_server/models.py b/geonode/qgis_server/models.py index 53133921de6..8b1692a7539 100644 --- a/geonode/qgis_server/models.py +++ b/geonode/qgis_server/models.py @@ -27,6 +27,7 @@ from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ +from geonode.security.models import PermissionLevelMixin from lxml import etree from geonode import qgis_server @@ -44,7 +45,7 @@ os.mkdir(QGIS_LAYER_DIRECTORY) -class QGISServerLayer(models.Model): +class QGISServerLayer(models.Model, PermissionLevelMixin): """Model for Layer in QGIS Server Backend. """ @@ -71,7 +72,8 @@ class QGISServerLayer(models.Model): 'QGISServerStyle', related_name='layer_default_style', default=None, - null=True) + null=True, + on_delete=models.SET_NULL) styles = models.ManyToManyField( 'QGISServerStyle', related_name='layer_styles') @@ -175,8 +177,16 @@ def delete_qgis_layer(self): for style in QGISServerStyle.objects.filter(layer_styles=None): style.delete() + def get_self_resource(self): + """Get associated resource base.""" + # Associate this model with resource + try: + return self.layer.get_self_resource() + except: + return None + -class QGISServerStyle(models.Model): +class QGISServerStyle(models.Model, PermissionLevelMixin): """Model wrapper for QGIS Server styles.""" name = models.CharField(_('style name'), max_length=255) @@ -296,8 +306,18 @@ def style_tile_cache_path(self): return os.path.join( QGIS_TILES_DIRECTORY, self.layer_styles.first().layer.name, self.name) + def get_self_resource(self): + """Get associated resource base.""" + # Associate this model with resource + try: + qgis_layer = self.layer_styles.first() + """:type: QGISServerLayer""" + return qgis_layer.get_self_resource() + except: + return None + -class QGISServerMap(models.Model): +class QGISServerMap(models.Model, PermissionLevelMixin): """Model wrapper for QGIS Server Map.""" map = models.OneToOneField( @@ -347,6 +367,14 @@ def cache_path(self): """ return os.path.join(QGIS_TILES_DIRECTORY, self.qgis_map_name) + def get_self_resource(self): + """Get associated resource base.""" + # Associate this model with resource + try: + return self.layer.get_self_resource() + except: + return None + from geonode.qgis_server.signals import \ register_qgis_server_signals # noqa: F402,F401 diff --git a/geonode/qgis_server/tests/data/test_grid.qml b/geonode/qgis_server/tests/data/test_grid.qml new file mode 100644 index 00000000000..217a5921214 --- /dev/null +++ b/geonode/qgis_server/tests/data/test_grid.qml @@ -0,0 +1,16 @@ + + + + + +-9999 +-236.1545301164897 +StretchToMinimumMaximum + + + + + + +0 + diff --git a/geonode/qgis_server/tests/test_views.py b/geonode/qgis_server/tests/test_views.py index e5be43d4758..592df6b7b43 100644 --- a/geonode/qgis_server/tests/test_views.py +++ b/geonode/qgis_server/tests/test_views.py @@ -171,7 +171,7 @@ def test_ogc_specific_layer(self): reverse('qgis_server:set-thumbnail', kwargs=params), data=data) # User dont have permission - self.assertEqual(response.status_code, 401) + self.assertEqual(response.status_code, 403) # Should log in self.client.login(username='admin', password='admin') response = self.client.post( @@ -291,6 +291,11 @@ class QGISServerStyleManagerTest(LiveServerTestCase): def setUp(self): call_command('loaddata', 'people_data', verbosity=0) + def data_path(self, path): + project_root = os.path.abspath(settings.PROJECT_ROOT) + return os.path.join( + project_root, 'qgis_server/tests/data', path) + @on_ogc_backend(qgis_server.BACKEND_PACKAGE) def test_list_style(self): """Test querying list of styles from QGIS Server.""" @@ -304,8 +309,95 @@ def test_list_style(self): # There will be a default style self.assertEqual( set(expected_list_style), - set([style.name for style in actual_list_style]) - ) + set([style.name for style in actual_list_style])) + + style_list_url = reverse( + 'qgis_server:download-qml', + kwargs={ + 'layername': layer.name + }) + response = self.client.get(style_list_url) + self.assertEqual(response.status_code, 200) + actual_list_style = json.loads(response.content) + + # There will be a default style + self.assertEqual( + set(expected_list_style), + set([style['name'] for style in actual_list_style])) + + layer.delete() + + @on_ogc_backend(qgis_server.BACKEND_PACKAGE) + def test_add_delete_style(self): + """Test add new style using qgis_server views.""" + filename = os.path.join(gisdata.GOOD_DATA, 'raster/test_grid.tif') + layer = file_upload(filename) + """:type: geonode.layers.models.Layer""" + + self.client.login(username='admin', password='admin') + + qml_path = self.data_path('test_grid.qml') + add_style_url = reverse( + 'qgis_server:upload-qml', + kwargs={ + 'layername': layer.name}) + + with open(qml_path) as file_handle: + form_data = { + 'name': 'new_style', + 'title': 'New Style', + 'qml': file_handle + } + response = self.client.post( + add_style_url, + data=form_data) + + self.assertEqual(response.status_code, 201) + + actual_list_style = style_list(layer, internal=False) + + expected_list_style = ['default', 'new_style'] + + self.assertEqual( + set(expected_list_style), + set([style.name for style in actual_list_style])) + + # Test delete request + delete_style_url = reverse( + 'qgis_server:remove-qml', + kwargs={ + 'layername': layer.name, + 'style_name': 'default'}) + + response = self.client.delete(delete_style_url) + self.assertEqual(response.status_code, 200) + + actual_list_style = style_list(layer, internal=False) + + expected_list_style = ['new_style'] + + self.assertEqual( + set(expected_list_style), + set([style.name for style in actual_list_style])) + + # Check new default + default_style_url = reverse( + 'qgis_server:default-qml', + kwargs={ + 'layername': layer.name}) + + response = self.client.get(default_style_url) + + self.assertEqual(response.status_code, 200) + expected_default_style_retval = { + 'name': 'new_style', + } + actual_default_style_retval = json.loads(response.content) + + for key, value in expected_default_style_retval.iteritems(): + self.assertEqual(actual_default_style_retval[key], value) + + layer.delete() class ThumbnailGenerationTest(LiveServerTestCase): diff --git a/geonode/qgis_server/urls.py b/geonode/qgis_server/urls.py index 0297511d78a..f3b0cb4d67e 100644 --- a/geonode/qgis_server/urls.py +++ b/geonode/qgis_server/urls.py @@ -123,24 +123,24 @@ name='map-print' ), url( - r'^style/(?P[^/]*)(?:/(?P[^/]*))?/edit$', - qml_style, - name='upload-qml' + r'^style/default/(?P[^/]*)(?:/(?P[^/]*))?$', + default_qml_style, + name='default-qml' ), url( - r'^style/(?P[^/]*)(?:/(?P[^/]*))?$', + r'^style/upload/(?P[^/]*)(?:/(?P[^/]*))?$', qml_style, - name='download-qml' + name='upload-qml' ), url( - r'^style/(?P[^/]*)(?:/(?P[^/]*))?$', + r'^style/remove/(?P[^/]*)/(?P[^/]*)$', qml_style, name='remove-qml' ), url( - r'^style/(?P[^/]*)/(?P[^/]*)/default$', - default_qml_style, - name='default-qml' + r'^style/download/(?P[^/]*)(?:/(?P[^/]*))?$', + qml_style, + name='download-qml' ), url( r'^thumbnail/set/(?P[^/]*)$', diff --git a/geonode/qgis_server/views.py b/geonode/qgis_server/views.py index 02603f09bf5..fed6da1ac29 100644 --- a/geonode/qgis_server/views.py +++ b/geonode/qgis_server/views.py @@ -498,7 +498,7 @@ def qml_style(request, layername, style_name=None): 'change_resourcebase', layer.get_self_resource()): return HttpResponse( 'User does not have permission to change QML style.', - status=401) + status=403) # Request about adding new QML style @@ -512,7 +512,7 @@ def qml_style(request, layername, style_name=None): 'resource': layer, 'style_upload_form': form }, - status=200).render() + status=400).render() try: uploaded_qml = request.FILES['qml'] @@ -561,7 +561,7 @@ def qml_style(request, layername, style_name=None): 'alert_message': response.content, 'alert_class': 'alert-danger' }, - status=200).render() + status=response.status_code).render() # We succeeded on adding new style @@ -583,7 +583,7 @@ def qml_style(request, layername, style_name=None): 'alert_class': 'alert-success', 'alert_message': alert_message }, - status=200).render() + status=201).render() except Exception as e: logger.exception(e) @@ -621,7 +621,7 @@ def qml_style(request, layername, style_name=None): 'alert_message': alert_message, 'alert_class': 'alert-danger' }, - status=200).render() + status=response.status_code).render() # Successfully removed styles # Handle when default style is deleted. @@ -645,7 +645,7 @@ def qml_style(request, layername, style_name=None): return HttpResponseBadRequest() -def default_qml_style(request, layername, style_name): +def default_qml_style(request, layername, style_name=None): """Set default style used by layer. :param layername: The layer name in Geonode. @@ -667,6 +667,16 @@ def default_qml_style(request, layername, style_name): return HttpResponse( json.dumps(retval), content_type='application/json') elif request.method == 'POST': + # For people who uses API request + if not request.user.has_perm( + 'change_resourcebase', layer.get_self_resource()): + return HttpResponse( + 'User does not have permission to change QML style.', + status=403) + + if not style_name: + return HttpResponseBadRequest() + style_url = style_set_default_url(layer, style_name) response = requests.get(style_url) @@ -715,7 +725,7 @@ def set_thumbnail(request, layername): 'change_resourcebase', layer.get_self_resource()): return HttpResponse( 'User does not have permission to change thumbnail.', - status=401) + status=403) # extract bbox bbox_string = request.POST['bbox'] diff --git a/geonode/static/geonode/js/upload/LayerInfo.js b/geonode/static/geonode/js/upload/LayerInfo.js index b2f00325283..112b2d20252 100644 --- a/geonode/static/geonode/js/upload/LayerInfo.js +++ b/geonode/static/geonode/js/upload/LayerInfo.js @@ -339,6 +339,10 @@ define(function (require, exports) { var b = '' + gettext('Edit Metadata') + ''; var c = '' + gettext('Upload Metadata') + ''; var d = '' + gettext('Manage Styles') + ''; + if(resp.ogc_backend == 'geonode.qgis_server'){ + // QGIS Server has no manage style interaction. + d = ''; + } var msg_col = ""; if (resp.info){ var msg_template = gettext('The column %1 was renamed to %2
'); diff --git a/geonode/upload/forms.py b/geonode/upload/forms.py index 38fda01cdd4..0e5351ab7f9 100644 --- a/geonode/upload/forms.py +++ b/geonode/upload/forms.py @@ -24,9 +24,12 @@ from django import forms from django.conf import settings from django.core.exceptions import ValidationError + +from geonode import geoserver, qgis_server from geonode.layers.forms import JSONField from geonode.upload.models import UploadFile from geonode.geoserver.helpers import ogc_server_settings +from geonode.utils import check_ogc_backend class UploadFileForm(forms.ModelForm): @@ -43,9 +46,9 @@ class LayerUploadForm(forms.Form): prj_file = forms.FileField(required=False) xml_file = forms.FileField(required=False) - if 'geonode.geoserver' in settings.INSTALLED_APPS: + if check_ogc_backend(geoserver.BACKEND_PACKAGE): sld_file = forms.FileField(required=False) - if 'geonode.qgis_server' in settings.INSTALLED_APPS: + if check_ogc_backend(qgis_server.BACKEND_PACKAGE): qml_file = forms.FileField(required=False) geogig = forms.BooleanField(required=False) @@ -77,9 +80,9 @@ class LayerUploadForm(forms.Form): "xml_file", ] # Adding style file based on the backend - if 'geonode.geoserver' in settings.INSTALLED_APPS: + if check_ogc_backend(geoserver.BACKEND_PACKAGE): spatial_files.append('sld_file') - if 'geonode.qgis_server' in settings.INSTALLED_APPS: + if check_ogc_backend(qgis_server.BACKEND_PACKAGE): spatial_files.append('qml_file') spatial_files = tuple(spatial_files) From f7c63d6da8673eb24131854770c4432fcf3c1592 Mon Sep 17 00:00:00 2001 From: Rizky Maulana Nugraha Date: Mon, 28 Aug 2017 18:13:07 +0700 Subject: [PATCH 2/8] Improvement for Styles API - Fix unittest for Geoserver Styles API - Add Delete method for Styles - Add Patch method for Layers to set default style --- geonode/api/api.py | 36 ++++++++++++++++++++ geonode/api/resourcebase_api.py | 52 ++++++++++++++++++++++++++++- geonode/api/tests.py | 59 +++++++++++++++++++++++++++++++-- pavement.py | 26 +++++++++++++-- 4 files changed, 167 insertions(+), 6 deletions(-) diff --git a/geonode/api/api.py b/geonode/api/api.py index 2230197b7b9..ce92cb50b58 100644 --- a/geonode/api/api.py +++ b/geonode/api/api.py @@ -455,6 +455,7 @@ class Meta: queryset = QGISServerStyle.objects.all() resource_name = 'styles' detail_uri_name = 'id' + allowed_methods = ['get', 'post', 'delete'] authorization = DjangoAuthorization() filtering = { 'id': ALL, @@ -574,6 +575,40 @@ def post_list(self, request, **kwargs): request, response.content, response_class=response_class) + def delete_detail(self, request, **kwargs): + """Attempt to redirect to QGIS Server Style management.""" + from geonode.qgis_server.views import qml_style + style_id = kwargs.get('id') + + qgis_style = QGISServerStyle.objects.get(id=style_id) + layername = qgis_style.layer_styles.first().layer.name + + response = qml_style(request, layername, style_name=qgis_style.name) + + if isinstance(response, TemplateResponse): + if response.status_code == 200: + # style deleted + return http.HttpNoContent() + else: + context = response.context_data + # Check form valid + style_upload_form = context['style_upload_form'] + if not style_upload_form.is_valid(): + raise BadRequest(style_upload_form.errors.as_text()) + alert_message = context['alert_message'] + raise BadRequest(alert_message) + elif isinstance(response, HttpResponse): + response_class = None + if response.status_code == 403: + response_class = http.HttpForbidden + return self.error_response( + request, response.content, + response_class=response_class) + + def delete_list(self, request, **kwargs): + """Do not allow delete list""" + return http.HttpForbidden() + class GeoserverStyleResource(ModelResource): """Styles API for Geoserver backend.""" @@ -601,6 +636,7 @@ class Meta: resource_name = 'styles' detail_uri_name = 'id' authorization = DjangoAuthorization() + allowed_methods = ['get'] filtering = { 'id': ALL, 'title': ALL, diff --git a/geonode/api/resourcebase_api.py b/geonode/api/resourcebase_api.py index b3eadb7fc1e..16a859c431f 100644 --- a/geonode/api/resourcebase_api.py +++ b/geonode/api/resourcebase_api.py @@ -17,11 +17,15 @@ # along with this program. If not, see . # ######################################################################### - +import json import re + +from django.core.urlresolvers import resolve from django.db.models import Q from django.http import HttpResponse from django.conf import settings +from django.template.response import TemplateResponse +from tastypie import http from tastypie.bundle import Bundle from tastypie.constants import ALL, ALL_WITH_RELATIONS @@ -685,6 +689,51 @@ def build_bundle( request=request, objects_saved=objects_saved) + def patch_detail(self, request, **kwargs): + """Allow patch request to update default_style. + + Request body must match this: + + { + 'default_style': + } + + """ + reason = 'Can only patch "default_style" field.' + try: + body = json.loads(request.body) + if 'default_style' not in body: + return http.HttpBadRequest(reason=reason) + match = resolve(body['default_style']) + style_id = match.kwargs['id'] + api_name = match.kwargs['api_name'] + resource_name = match.kwargs['resource_name'] + if not (resource_name == 'styles' and api_name == 'api'): + raise Exception() + + from geonode.qgis_server.models import QGISServerStyle + + style = QGISServerStyle.objects.get(id=style_id) + + layer_id = kwargs['id'] + layer = Layer.objects.get(id=layer_id) + except: + return http.HttpBadRequest(reason=reason) + + from geonode.qgis_server.views import default_qml_style + + request.method = 'POST' + response = default_qml_style( + request, + layername=layer.name, + style_name=style.name) + + if isinstance(response, TemplateResponse): + if response.status_code == 200: + return HttpResponse(status=200) + + return self.error_response(request, response.content) + class Meta(CommonMetaApi): queryset = Layer.objects.distinct().order_by('-date') if settings.RESOURCE_PUBLISHING: @@ -692,6 +741,7 @@ class Meta(CommonMetaApi): resource_name = 'layers' detail_uri_name = 'id' include_resource_uri = True + allowed_methods = ['get', 'patch'] excludes = ['csw_anytext', 'metadata_xml'] filtering = CommonMetaApi.filtering # Allow filtering using ID diff --git a/geonode/api/tests.py b/geonode/api/tests.py index d8c5af32f23..58c4d5cb38b 100644 --- a/geonode/api/tests.py +++ b/geonode/api/tests.py @@ -17,6 +17,7 @@ # along with this program. If not, see . # ######################################################################### +import json import os from StringIO import StringIO @@ -30,7 +31,8 @@ from geonode.layers.utils import file_upload from tastypie.test import ResourceTestCase -from geonode.base.populate_test_data import create_models, all_public +from geonode.base.populate_test_data import create_models, all_public, \ + reconnect_signals, disconnect_signals from geonode.layers.models import Layer from geonode.utils import check_ogc_backend @@ -247,6 +249,10 @@ class LayersStylesApiInteractionTests(LiveServerTestCase, ResourceTestCase): def setUp(self): super(LayersStylesApiInteractionTests, self).setUp() + # Reconnect Geoserver signals + if check_ogc_backend(geoserver.BACKEND_PACKAGE): + reconnect_signals() + call_command('loaddata', 'people_data', verbosity=0) self.layer_list_url = reverse( @@ -266,7 +272,10 @@ def setUp(self): def tearDown(self): Layer.objects.all().delete() - @on_ogc_backend(qgis_server.BACKEND_PACKAGE) + # Disconnect Geoserver signals + if check_ogc_backend(geoserver.BACKEND_PACKAGE): + disconnect_signals() + def test_layer_interaction(self): """Layer API interaction check.""" layer_id = self.layer.id @@ -318,7 +327,6 @@ def test_layer_interaction(self): self.assertEqual(obj, prev_obj) - @on_ogc_backend(qgis_server.BACKEND_PACKAGE) def test_style_interaction(self): """Style API interaction check.""" @@ -451,3 +459,48 @@ def test_add_delete_styles(self): objects = self.deserialize(resp)['objects'] self.assertEqual(len(objects), 2) + + # Attempt to set default style + resp = self.api_client.get(layer_detail_url) + self.assertValidJSONResponse(resp) + obj = self.deserialize(resp) + # Get style list and get new default style + styles = obj['styles'] + new_default_style = None + for s in styles: + if not s == obj['default_style']: + new_default_style = s + break + obj['default_style'] = new_default_style + # Put the new update + patch_data = { + 'default_style': new_default_style + } + resp = self.client.patch( + layer_detail_url, + data=json.dumps(patch_data), + content_type='application/json') + self.assertEqual(resp.status_code, 200) + + # check new default_style + resp = self.api_client.get(layer_detail_url) + self.assertValidJSONResponse(resp) + obj = self.deserialize(resp) + self.assertEqual(obj['default_style'], new_default_style) + + # Attempt to delete style + filter_url = style_list_url + '?layer__id=%d&name=%s' % ( + self.layer.id, data['name']) + resp = self.api_client.get(filter_url) + self.assertValidJSONResponse(resp) + objects = self.deserialize(resp)['objects'] + + resource_uri = objects[0]['resource_uri'] + + resp = self.client.delete(resource_uri) + self.assertEqual(resp.status_code, 204) + + resp = self.api_client.get(filter_url) + meta = self.deserialize(resp)['meta'] + + self.assertEqual(meta['total_count'], 0) diff --git a/pavement.py b/pavement.py index ce1032837db..9c8f2b8dcbc 100644 --- a/pavement.py +++ b/pavement.py @@ -436,9 +436,31 @@ def test(options): """ Run GeoNode's Unit Test Suite """ - sh("%s manage.py test %s.tests --noinput --liveserver=0.0.0.0:8000" % ( - options.get('prefix'), '.tests '.join(GEONODE_APPS))) + from geonode.settings import INSTALLED_APPS + success = False + + if 'geonode.geoserver' in INSTALLED_APPS: + _reset() + # Start GeoServer + call_task('start_geoserver') + info("GeoNode is now available, running the tests now.") + + try: + sh("%s manage.py test %s.tests --noinput --liveserver=0.0.0.0:8000" % ( + options.get('prefix'), '.tests '.join(GEONODE_APPS))) + except BuildFailure as e: + info('Tests failed! %s' % str(e)) + else: + success = True + finally: + if 'geonode.geoserver' in INSTALLED_APPS: + # don't use call task here - it won't run since it already has + stop() + + _reset() + if not success: + sys.exit(1) @task def test_javascript(options): From c7615e75ea671456dddd353c4df490c36da0e084 Mon Sep 17 00:00:00 2001 From: Rizky Maulana Nugraha Date: Thu, 31 Aug 2017 13:47:18 +0700 Subject: [PATCH 3/8] Small patch for Layers API - Code improvement to control Links field --- geonode/api/resourcebase_api.py | 20 +++++++++----------- geonode/api/tests.py | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/geonode/api/resourcebase_api.py b/geonode/api/resourcebase_api.py index 16a859c431f..4b3d028e2b3 100644 --- a/geonode/api/resourcebase_api.py +++ b/geonode/api/resourcebase_api.py @@ -578,6 +578,7 @@ class LayerResource(CommonModelApi): links = fields.ListField( attribute='links', null=True, + use_in='all', default=[]) if check_ogc_backend(qgis_server.BACKEND_PACKAGE): default_style = fields.ForeignKey( @@ -618,8 +619,12 @@ def format_objects(self, objects): # provide style information bundle = self.build_bundle(obj=obj) - formatted_obj['default_style'] = ( - self.default_style.dehydrate(bundle, for_list=True)) + formatted_obj['default_style'] = self.default_style.dehydrate( + bundle, for_list=True) + + if self.links.use_in == 'all' or self.links.use_in == 'list': + formatted_obj['links'] = self.dehydrate_links( + bundle) # Add resource uri formatted_obj['resource_uri'] = self.get_resource_uri(bundle) # put the object on the response stack @@ -628,6 +633,8 @@ def format_objects(self, objects): def dehydrate_links(self, bundle): """Dehydrate links field.""" + + dehydrated = [] obj = bundle.obj link_fields = [ 'extension', @@ -636,21 +643,12 @@ def dehydrate_links(self, bundle): 'mime', 'url' ] - dehydrated = [] for l in obj.link_set.all(): formatted_link = model_to_dict(l, fields=link_fields) dehydrated.append(formatted_link) return dehydrated - def dehydrate(self, bundle): - """Override dehydrate phase""" - - # Override Link dehydrate phase - bundle.data[self.links.instance_name] = self.dehydrate_links(bundle) - - return bundle - def populate_object(self, obj): """Populate results with necessary fields diff --git a/geonode/api/tests.py b/geonode/api/tests.py index 58c4d5cb38b..c3da4df53d3 100644 --- a/geonode/api/tests.py +++ b/geonode/api/tests.py @@ -306,8 +306,8 @@ def test_layer_interaction(self): objects = self.deserialize(resp)['objects'] self.assertEqual(len(objects), 1) obj = objects[0] - # Should not have links (to save payload from big text) - self.assertTrue('links' not in obj) + # Should have links + self.assertTrue('links' in obj) # Should not have styles self.assertTrue('styles' not in obj) # Should have default_style From 32591320c31d279acc469f54024be736af3396c5 Mon Sep 17 00:00:00 2001 From: Boney Bun Date: Tue, 12 Sep 2017 02:39:14 +0700 Subject: [PATCH 4/8] add download QGS layer file (#322) * add download QGS layer file * add Links in post save signal note: these code depend on kartoza/otf-project#11 fix #157 --- geonode/qgis_server/signals.py | 20 ++++++++++++++++ geonode/qgis_server/urls.py | 6 +++++ geonode/qgis_server/views.py | 42 ++++++++++++++++++++++++++++++++++ geonode/settings.py | 2 ++ 4 files changed, 70 insertions(+) diff --git a/geonode/qgis_server/signals.py b/geonode/qgis_server/signals.py index 76a2ec56632..48a414256ab 100644 --- a/geonode/qgis_server/signals.py +++ b/geonode/qgis_server/signals.py @@ -215,6 +215,26 @@ def qgis_server_post_save(instance, sender, **kwargs): ) ) + # QGS link layer workspace + ogc_qgs_url = urljoin( + base_url, + reverse( + 'qgis_server:download-qgs', + kwargs={'layername': instance.name})) + logger.debug('qgs_download_url: %s' % ogc_qgs_url) + link_name = 'QGS Layer file' + link_mime = 'application/xml' + Link.objects.update_or_create( + resource=instance.resourcebase_ptr, + name=link_name, + defaults=dict( + extension='qgs', + mime=link_mime, + url=ogc_qgs_url, + link_type='data' + ) + ) + if instance.is_vector(): # WFS link layer workspace ogc_wfs_url = urljoin( diff --git a/geonode/qgis_server/urls.py b/geonode/qgis_server/urls.py index f3b0cb4d67e..4a26bc6376c 100644 --- a/geonode/qgis_server/urls.py +++ b/geonode/qgis_server/urls.py @@ -22,6 +22,7 @@ from geonode.qgis_server.views import ( download_zip, + download_qgs, tile, tile_404, legend, @@ -42,6 +43,11 @@ download_zip, name='download-zip' ), + url( + r'^download-qgs/(?P[\w]*)$', + download_qgs, + name='download-qgs' + ), url( r'^tiles/' r'(?P[^/]*)/' diff --git a/geonode/qgis_server/views.py b/geonode/qgis_server/views.py index fed6da1ac29..416a8e89888 100644 --- a/geonode/qgis_server/views.py +++ b/geonode/qgis_server/views.py @@ -106,6 +106,48 @@ def download_zip(request, layername): return resp +def download_qgs(request, layername): + """Download QGS file for a layer. + + :param layername: The request from frontend. + :type layername: HttpRequest + + :param layername: The layer name in Geonode. + :type layername: basestring + + :return: QGS file. + """ + + layer = get_object_or_404(Layer, name=layername) + ogc_url = reverse('qgis_server:layer-request', + kwargs={'layername': layername}) + url = settings.SITEURL + ogc_url.replace("/", "", 1) + + layers = [{ + 'type': 'raster', + 'display': layername, + 'driver': 'wms', + 'crs': 'EPSG:4326', + 'format': 'image/png', + 'styles': '', + 'layers': layer.title, + 'url': url + }] + + json_layers = json.dumps(layers) + + url_server = settings.QGIS_SERVER_URL + \ + '?SERVICE=PROJECTDEFINITIONS&LAYERS=' + json_layers + request = requests.get(url_server) + response = HttpResponse( + request.content, content_type="application/xml", + status=request.status_code) + response['Content-Disposition'] = \ + 'attachment; filename=%s' % layer.title + '.qgs' + + return response + + def legend(request, layername, layertitle=False, style=None): """Get the legend from a layer. diff --git a/geonode/settings.py b/geonode/settings.py index 5a185ce6112..9c7558f83d2 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -867,6 +867,7 @@ DOWNLOAD_FORMATS_VECTOR = [ 'JPEG', 'PDF', 'PNG', 'Zipped Shapefile', 'GML 2.0', 'GML 3.1.1', 'CSV', 'Excel', 'GeoJSON', 'KML', 'View in Google Earth', 'Tiles', + 'QGS Layer file', ] DOWNLOAD_FORMATS_RASTER = [ 'JPEG', @@ -881,6 +882,7 @@ 'Tiles', 'GML', 'GZIP', + 'QGS Layer file', 'Zipped All Files' ] From 63f051b41b9cd2025afc811cd0e3bf75222cc287 Mon Sep 17 00:00:00 2001 From: Boney Bun Date: Tue, 12 Sep 2017 10:44:06 +0700 Subject: [PATCH 5/8] download a QLR file for a layer (#320) * download a QLR file for a layer * use Links to add url on the UI The QLR would be able to open using QGIS desktop this code should work along with kartoza/otf-project#10 fix #154 --- geonode/qgis_server/signals.py | 20 ++++++++++++++++++ geonode/qgis_server/urls.py | 6 ++++++ geonode/qgis_server/views.py | 38 ++++++++++++++++++++++++++++++++++ geonode/settings.py | 2 ++ 4 files changed, 66 insertions(+) diff --git a/geonode/qgis_server/signals.py b/geonode/qgis_server/signals.py index 48a414256ab..8f4f4e4a226 100644 --- a/geonode/qgis_server/signals.py +++ b/geonode/qgis_server/signals.py @@ -256,6 +256,26 @@ def qgis_server_post_save(instance, sender, **kwargs): ) ) + # QLR link layer workspace + ogc_qlr_url = urljoin( + base_url, + reverse( + 'qgis_server:download-qlr', + kwargs={'layername': instance.name})) + logger.debug('qlr_download_url: %s' % ogc_qlr_url) + link_name = 'QGIS Layer file' + link_mime = 'application/xml' + Link.objects.update_or_create( + resource=instance.resourcebase_ptr, + name=link_name, + defaults=dict( + extension='qlr', + mime=link_mime, + url=ogc_qlr_url, + link_type='data' + ) + ) + # if layer has overwrite attribute, then it probably comes from # importlayers management command and needs to be overwritten overwrite = getattr(instance, 'overwrite', False) diff --git a/geonode/qgis_server/urls.py b/geonode/qgis_server/urls.py index 4a26bc6376c..6988ce2ad20 100644 --- a/geonode/qgis_server/urls.py +++ b/geonode/qgis_server/urls.py @@ -31,6 +31,7 @@ qgis_server_pdf, qgis_server_map_print, geotiff, + download_qlr, qml_style, set_thumbnail, default_qml_style) @@ -153,4 +154,9 @@ set_thumbnail, name='set-thumbnail' ), + url( + r'^download-qlr/(?P[\w]*)$', + download_qlr, + name='download-qlr' + ), ) diff --git a/geonode/qgis_server/views.py b/geonode/qgis_server/views.py index 416a8e89888..a804b20ee33 100644 --- a/geonode/qgis_server/views.py +++ b/geonode/qgis_server/views.py @@ -782,3 +782,41 @@ def set_thumbnail(request, layername): } return HttpResponse( json.dumps(retval), content_type="application/json") + + +def download_qlr(request, layername): + """Download QLR file for a layer. + + :param layername: The layer name in Geonode. + :type layername: basestring + + :return: QLR file. + """ + layer = get_object_or_404(Layer, name=layername) + ogc_url = reverse('qgis_server:layer-request', + kwargs={'layername': layername}) + url = settings.SITEURL + ogc_url.replace("/", "", 1) + + layers = [{ + 'type': 'raster', + 'display': layername, + 'driver': 'wms', + 'crs': 'EPSG:4326', + 'format': 'image/png', + 'styles': '', + 'layers': layer.title, + 'url': url + }] + json_layers = json.dumps(layers) + + url_server = settings.QGIS_SERVER_URL + \ + '?SERVICE=LAYERDEFINITIONS&LAYERS=' + json_layers + request = requests.get(url_server) + response = HttpResponse( + request.content, + content_type="application/xml", + status=request.status_code) + response['Content-Disposition'] = \ + 'attachment; filename=%s' % layer.title + '.qlr' + + return response diff --git a/geonode/settings.py b/geonode/settings.py index 9c7558f83d2..beb022f77f5 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -867,6 +867,7 @@ DOWNLOAD_FORMATS_VECTOR = [ 'JPEG', 'PDF', 'PNG', 'Zipped Shapefile', 'GML 2.0', 'GML 3.1.1', 'CSV', 'Excel', 'GeoJSON', 'KML', 'View in Google Earth', 'Tiles', + 'QGIS Layer file', 'QGS Layer file', ] DOWNLOAD_FORMATS_RASTER = [ @@ -882,6 +883,7 @@ 'Tiles', 'GML', 'GZIP', + 'QGIS Layer file', 'QGS Layer file', 'Zipped All Files' ] From 546c23678db204ea8354b3a52e2d502b3f4ff58b Mon Sep 17 00:00:00 2001 From: Boney Bun Date: Thu, 14 Sep 2017 10:02:21 +0700 Subject: [PATCH 6/8] embed iframe when viewing a map (#313) embed iframe when viewing a map fix #152 --- geonode/maps/qgis_server_views.py | 46 +++++++++++++++++++ .../templates/leaflet_maps/map_detail.html | 23 ++++++++++ geonode/maps/templates/maps/map_detail.html | 43 +++++++++++++++++ geonode/maps/urls.py | 7 ++- 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 geonode/maps/templates/leaflet_maps/map_detail.html diff --git a/geonode/maps/qgis_server_views.py b/geonode/maps/qgis_server_views.py index 7f35062b7c0..219566f0662 100644 --- a/geonode/maps/qgis_server_views.py +++ b/geonode/maps/qgis_server_views.py @@ -96,3 +96,49 @@ def get_context_data(self, **kwargs): def get_object(self): return Map.objects.get(id=self.kwargs.get("mapid")) + + +class MapEmbedView(DetailView): + model = Map + template_name = 'leaflet_maps/map_detail.html' + context_object_name = 'map' + + def get_context_data(self, **kwargs): + """Prepare context data.""" + + mapid = self.kwargs.get('mapid') + snapshot = self.kwargs.get('snapshot') + request = self.request + + map_obj = _resolve_map( + request, mapid, 'base.view_resourcebase', _PERMISSION_MSG_VIEW) + + if 'access_token' in request.session: + access_token = request.session['access_token'] + else: + access_token = None + + if snapshot is None: + config = map_obj.viewer_json(request.user, access_token) + else: + config = snapshot_config(snapshot, map_obj, request.user, + access_token) + # list all required layers + layers = Layer.objects.all() + map_layers = MapLayer.objects.filter( + map_id=mapid).order_by('stack_order') + context = { + 'config': json.dumps(config), + 'create': False, + 'layers': layers, + 'resource': map_obj, + 'map_layers': map_layers, + 'preview': getattr( + settings, + 'LAYER_PREVIEW_LIBRARY', + '') + } + return context + + def get_object(self): + return Map.objects.get(id=self.kwargs.get("mapid")) diff --git a/geonode/maps/templates/leaflet_maps/map_detail.html b/geonode/maps/templates/leaflet_maps/map_detail.html new file mode 100644 index 00000000000..38e93acd80f --- /dev/null +++ b/geonode/maps/templates/leaflet_maps/map_detail.html @@ -0,0 +1,23 @@ + {% if TWITTER_CARD %} + {% include "base/_resourcebase_twittercard.html" %} + {% endif %} + {% if OPENGRAPH_ENABLED %} + {% include "base/_resourcebase_opengraph.html" %} + {% endif %} + {% if preview == 'OL3' %} + {% include "maps/map_ol3.html" %} + {% elif preview == 'leaflet' %} + {% include "maps/map_leaflet.html" %} + {% elif preview == 'react' %} + {% include 'geonode-client/map_detail.html' %} + {% else %} + {% include "maps/map_include.html" %} + {% endif %} + {{ block.super }} + + + +
+
+
+ diff --git a/geonode/maps/templates/maps/map_detail.html b/geonode/maps/templates/maps/map_detail.html index 090f07eefab..ddf12557e5c 100644 --- a/geonode/maps/templates/maps/map_detail.html +++ b/geonode/maps/templates/maps/map_detail.html @@ -209,6 +209,25 @@

{% trans "Copy this map" %}

{% trans "Duplicate this map and modify it for your own purposes" %}

{% trans "Create a New Map" %} + {% if 'geonode.qgis_server' in INSTALLED_APPS %} +
  • +

    {% trans "Embed this map" %}

    +

    {% trans "Embed this map to your own sites" %}

    +
    + + +
    +
  • + {% endif %} + {% if resource.is_public and "change_resourcebase" in perms_list or resource.layer_group %}
  • @@ -233,6 +252,30 @@

    {% trans "Map WMS" %}

    + + + {% endblock %} {% block extra_script %} diff --git a/geonode/maps/urls.py b/geonode/maps/urls.py index 0f4baafbac5..3f7c89dde14 100644 --- a/geonode/maps/urls.py +++ b/geonode/maps/urls.py @@ -22,7 +22,8 @@ from django.views.generic import TemplateView from geonode import geoserver, qgis_server -from geonode.maps.qgis_server_views import MapCreateView, MapDetailView +from geonode.maps.qgis_server_views import MapCreateView, \ + MapDetailView, MapEmbedView from geonode.utils import check_ogc_backend js_info_dict = { @@ -35,10 +36,12 @@ if check_ogc_backend(geoserver.BACKEND_PACKAGE): new_map_view = 'new_map' existing_map_view = 'map_view' + map_embed = 'map_embed' elif check_ogc_backend(qgis_server.BACKEND_PACKAGE): new_map_view = MapCreateView.as_view() existing_map_view = MapDetailView.as_view() + map_embed = MapEmbedView.as_view() urlpatterns = patterns( 'geonode.maps.views', @@ -60,7 +63,7 @@ url(r'^(?P[^/]+)/remove$', 'map_remove', name='map_remove'), url(r'^(?P[^/]+)/metadata$', 'map_metadata', name='map_metadata'), url(r'^(?P[^/]+)/metadata_advanced$', 'map_metadata_advanced', name='map_metadata_advanced'), - url(r'^(?P[^/]+)/embed$', 'map_embed', name='map_embed'), + url(r'^(?P[^/]+)/embed$', map_embed, name='map_embed'), url(r'^(?P[^/]+)/history$', 'ajax_snapshot_history'), url(r'^(?P\d+)/thumbnail$', 'map_thumbnail', name='map_thumbnail'), url(r'^(?P[^/]+)/(?P[A-Za-z0-9_\-]+)/view$', 'map_view'), From 0c36c84735b969e96c87194add96d5243b5e505a Mon Sep 17 00:00:00 2001 From: Boney Bun Date: Fri, 15 Sep 2017 11:07:37 +0700 Subject: [PATCH 7/8] bug fix on qgs and qlr layer (#326) * bug fix on qgs and qlr layer: using layername instead of title * code refactoring: url is generated on the helpers.py --- geonode/qgis_server/helpers.py | 93 ++++++++++++++++++++++++++++++++++ geonode/qgis_server/views.py | 51 ++++--------------- 2 files changed, 102 insertions(+), 42 deletions(-) diff --git a/geonode/qgis_server/helpers.py b/geonode/qgis_server/helpers.py index c67eba5de41..5e0c5f86a4b 100644 --- a/geonode/qgis_server/helpers.py +++ b/geonode/qgis_server/helpers.py @@ -23,6 +23,7 @@ import re import shutil import urllib +import json from urlparse import urljoin import requests @@ -468,6 +469,98 @@ def legend_url(layer, layertitle=False, style=None, internal=True): return url +def qgs_url(layer, style=None, internal=True): + """Construct QGIS Server url to fetch QGS. + + :param layer: Layer to use + :type layer: Layer + + :param style: Layer style to choose + :type style: str + + :param internal: Flag to switch between public url and internal url. + Public url will be served by Django Geonode (proxified). + :type internal: bool + + :return: QGIS Server request url for QGS + :rtype: str + """ + qgis_server_url = qgis_server_endpoint(internal) + + ogc_url = reverse('qgis_server:layer-request', + kwargs={'layername': layer.name}) + url = settings.SITEURL + ogc_url.replace("/", "", 1) + + # for now, style always set to '' + if not style: + style = '' + else: + style = 'default' + + layers = [{ + 'type': 'raster', + 'display': layer.name, + 'driver': 'wms', + 'crs': 'EPSG:4326', + 'format': 'image/png', + 'styles': style, + 'layers': layer.name, + 'url': url + }] + + json_layers = json.dumps(layers) + + url_server = qgis_server_url + \ + '?SERVICE=PROJECTDEFINITIONS&LAYERS=' + json_layers + + return url_server + + +def qlr_url(layer, style=None, internal=True): + """Construct QGIS Server url to fetch QLR. + + :param layer: Layer to use + :type layer: Layer + + :param style: Layer style to choose + :type style: str + + :param internal: Flag to switch between public url and internal url. + Public url will be served by Django Geonode (proxified). + :type internal: bool + + :return: QGIS Server request url for QLR + :rtype: str + """ + qgis_server_url = qgis_server_endpoint(internal) + ogc_url = reverse('qgis_server:layer-request', + kwargs={'layername': layer.name}) + url = settings.SITEURL + ogc_url.replace("/", "", 1) + + # for now, style always set to '' + if not style: + style = '' + else: + style = 'default' + + layers = [{ + 'type': 'raster', + 'display': layer.name, + 'driver': 'wms', + 'crs': 'EPSG:4326', + 'format': 'image/png', + 'styles': style, + 'layers': layer.name, + 'url': url + }] + json_layers = json.dumps(layers) + + url_server = qgis_server_url + \ + '?SERVICE=LAYERDEFINITIONS&LAYERS=' + json_layers + + return url_server + + def wms_get_capabilities_url(layer=None, internal=True): """Construct WMS GetCapabilities request. diff --git a/geonode/qgis_server/views.py b/geonode/qgis_server/views.py index a804b20ee33..8a22586d07a 100644 --- a/geonode/qgis_server/views.py +++ b/geonode/qgis_server/views.py @@ -47,6 +47,8 @@ tile_url_format, legend_url, tile_url, + qgs_url, + qlr_url, qgis_server_endpoint, style_get_url, style_list, style_add_url, style_remove_url, style_set_default_url) from geonode.qgis_server.models import QGISServerLayer @@ -117,33 +119,14 @@ def download_qgs(request, layername): :return: QGS file. """ - layer = get_object_or_404(Layer, name=layername) - ogc_url = reverse('qgis_server:layer-request', - kwargs={'layername': layername}) - url = settings.SITEURL + ogc_url.replace("/", "", 1) - - layers = [{ - 'type': 'raster', - 'display': layername, - 'driver': 'wms', - 'crs': 'EPSG:4326', - 'format': 'image/png', - 'styles': '', - 'layers': layer.title, - 'url': url - }] - - json_layers = json.dumps(layers) - - url_server = settings.QGIS_SERVER_URL + \ - '?SERVICE=PROJECTDEFINITIONS&LAYERS=' + json_layers - request = requests.get(url_server) + url = qgs_url(layer, internal=True) + request = requests.get(url) response = HttpResponse( request.content, content_type="application/xml", status=request.status_code) response['Content-Disposition'] = \ - 'attachment; filename=%s' % layer.title + '.qgs' + 'attachment; filename=%s' % layer.name + '.qgs' return response @@ -793,30 +776,14 @@ def download_qlr(request, layername): :return: QLR file. """ layer = get_object_or_404(Layer, name=layername) - ogc_url = reverse('qgis_server:layer-request', - kwargs={'layername': layername}) - url = settings.SITEURL + ogc_url.replace("/", "", 1) - - layers = [{ - 'type': 'raster', - 'display': layername, - 'driver': 'wms', - 'crs': 'EPSG:4326', - 'format': 'image/png', - 'styles': '', - 'layers': layer.title, - 'url': url - }] - json_layers = json.dumps(layers) - - url_server = settings.QGIS_SERVER_URL + \ - '?SERVICE=LAYERDEFINITIONS&LAYERS=' + json_layers - request = requests.get(url_server) + url = qlr_url(layer, internal=True) + + request = requests.get(url) response = HttpResponse( request.content, content_type="application/xml", status=request.status_code) response['Content-Disposition'] = \ - 'attachment; filename=%s' % layer.title + '.qlr' + 'attachment; filename=%s' % layer.name + '.qlr' return response From 9064c11b7091548e245b65c30f744744251b220a Mon Sep 17 00:00:00 2001 From: Rizky Maulana Nugraha Date: Fri, 15 Sep 2017 17:02:55 +0700 Subject: [PATCH 8/8] Fix for xml metadata utilities - Allow to fail gracefully if the metadata structure is not expected --- geonode/qgis_server/xml_utilities.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/geonode/qgis_server/xml_utilities.py b/geonode/qgis_server/xml_utilities.py index 331c77f8c76..dd039cede47 100644 --- a/geonode/qgis_server/xml_utilities.py +++ b/geonode/qgis_server/xml_utilities.py @@ -103,7 +103,12 @@ def insert_xml_element(root, element_path): element = root.find(path, XML_NS) if element is None: # if a parent is missing insert it at the right place - element = ElementTree.SubElement(parent, tag) + try: + element = ElementTree.SubElement(parent, tag) + except: + # In some cases we can't add parent because the tag name is + # not specific + pass parent = element return element