From 34f95891f2941177fbd632e126bb8a8107c538bd Mon Sep 17 00:00:00 2001 From: Sylvain POULAIN Date: Fri, 6 Sep 2024 12:54:17 +0400 Subject: [PATCH 01/61] Profile icon proportions (#12550) --- geonode/static/geonode/less/base.less | 1 + 1 file changed, 1 insertion(+) diff --git a/geonode/static/geonode/less/base.less b/geonode/static/geonode/less/base.less index 548b2403d4c..e56f811f518 100644 --- a/geonode/static/geonode/less/base.less +++ b/geonode/static/geonode/less/base.less @@ -1008,6 +1008,7 @@ span.icon.node-icon:before { img { width: 100%; + height: 100%; } } From 926ae3def410ab9229bd7169f157a31d78fe6084 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Fri, 6 Sep 2024 16:23:17 +0200 Subject: [PATCH 02/61] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1f9fafe76cf..995563e77c9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,6 +15,7 @@ The following are required only for core and extension modules (they are welcome - [ ] The issue connected to the PR must have Labels and Milestone assigned - [ ] PR for bug fixes and small new features are presented as a single commit - [ ] Commit message must be in the form "[Fixes #] Title of the Issue" +- [ ] PR title must be in the form "[Fixes #] Title of the PR" - [ ] New unit tests have been added covering the changes, unless there is an explanation on why the tests are not necessary/implemented - [ ] This PR passes all existing unit tests (test results will be reported by travis-ci after opening this PR) - [ ] This PR passes the QA checks: black geonode && flake8 geonode From c21f14af4516f2d674fdeaac8b713d262e8a7ca5 Mon Sep 17 00:00:00 2001 From: "G. Allegri" Date: Fri, 6 Sep 2024 16:14:44 +0200 Subject: [PATCH 03/61] remove type control from uuid url --- geonode/catalogue/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geonode/catalogue/urls.py b/geonode/catalogue/urls.py index 29d03e49890..e5816e32268 100644 --- a/geonode/catalogue/urls.py +++ b/geonode/catalogue/urls.py @@ -34,5 +34,5 @@ views.csw_render_extra_format_html, name="csw_render_extra_format_html", ), - path(r"uuid/", views.resolve_uuid, name="resolve_uuid"), + path(r"uuid/", views.resolve_uuid, name="resolve_uuid"), ] From 7ea4846c67b7ddaabc975afb37516d4c0a6d0be6 Mon Sep 17 00:00:00 2001 From: Sylvain POULAIN Date: Fri, 13 Sep 2024 17:45:54 +0400 Subject: [PATCH 04/61] Duplicate "forgot password" in login page (#12572) --- geonode/templates/account/login.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/geonode/templates/account/login.html b/geonode/templates/account/login.html index b03e3631d6f..579d84fc4a2 100644 --- a/geonode/templates/account/login.html +++ b/geonode/templates/account/login.html @@ -40,9 +40,6 @@

{% trans "Log in to an existing account" %}

{% if redirect_field_value %} {% endif %} - From 4caddf7e289f9429d6fecc204e23c7f682b9412d Mon Sep 17 00:00:00 2001 From: Mattia Date: Tue, 17 Sep 2024 11:09:11 +0200 Subject: [PATCH 05/61] [Fixes #12585] change_dataset_style and change_dataset_data are not returned --- geonode/security/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geonode/security/models.py b/geonode/security/models.py index 0f6805a96d3..5ee7c7b1100 100644 --- a/geonode/security/models.py +++ b/geonode/security/models.py @@ -423,10 +423,10 @@ def calculate_perms(instance, user): perms = calculate_perms(self, user) if getattr(self, "get_real_instance", None): - perms.union(calculate_perms(self.get_real_instance(), user)) + perms = perms.union(calculate_perms(self.get_real_instance(), user)) if getattr(self, "get_self_resource", None): - perms.union(calculate_perms(self.get_self_resource(), user)) + perms = perms.union(calculate_perms(self.get_self_resource(), user)) perms_as_list = list(set(perms)) From f8c3f2b65bcdab241d57e37c55ce9ea9ee90b86a Mon Sep 17 00:00:00 2001 From: Mattia Date: Tue, 17 Sep 2024 11:19:51 +0200 Subject: [PATCH 06/61] [Fixes #12585] change_dataset_style and change_dataset_data are not returned --- geonode/security/tests.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/geonode/security/tests.py b/geonode/security/tests.py index 47b3c513c45..77bee1fcfcc 100644 --- a/geonode/security/tests.py +++ b/geonode/security/tests.py @@ -1857,6 +1857,8 @@ def test_set_compact_permissions(self): "view_resourcebase", "approve_resourcebase", "publish_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_manager: [], self.group_member: [], @@ -1882,6 +1884,8 @@ def test_set_compact_permissions(self): "view_resourcebase", "approve_resourcebase", "publish_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_manager: ["view_resourcebase", "publish_resourcebase", "approve_resourcebase"], self.group_member: ["view_resourcebase"], @@ -1891,6 +1895,8 @@ def test_set_compact_permissions(self): "download_resourcebase", "change_resourcebase_metadata", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.anonymous_user: ["view_resourcebase"], }, @@ -1926,6 +1932,8 @@ def test_permissions_are_set_as_expected_resource_publishing_True(self): "change_resourcebase_metadata", "change_resourcebase_permissions", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_manager: [ "change_resourcebase", @@ -1936,6 +1944,8 @@ def test_permissions_are_set_as_expected_resource_publishing_True(self): "view_resourcebase", "approve_resourcebase", "publish_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_member: ["download_resourcebase", "view_resourcebase"], self.not_group_member: [], @@ -1953,6 +1963,8 @@ def test_permissions_are_set_as_expected_resource_publishing_True(self): "change_resourcebase_metadata", "change_resourcebase_permissions", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_manager: [ "change_resourcebase", @@ -1963,6 +1975,8 @@ def test_permissions_are_set_as_expected_resource_publishing_True(self): "change_resourcebase_permissions", "approve_resourcebase", "publish_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_member: ["download_resourcebase", "view_resourcebase"], self.not_group_member: ["view_resourcebase"], @@ -2006,6 +2020,8 @@ def test_permissions_are_set_as_expected_admin_upload_resource_publishing_True(s "view_resourcebase", "approve_resourcebase", "publish_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_member: ["download_resourcebase", "view_resourcebase"], self.not_group_member: [], @@ -2028,6 +2044,8 @@ def test_permissions_are_set_as_expected_admin_upload_resource_publishing_True(s "view_resourcebase", "approve_resourcebase", "publish_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_member: ["download_resourcebase", "view_resourcebase"], self.not_group_member: ["view_resourcebase"], @@ -2076,6 +2094,8 @@ def test_permissions_are_set_as_expected_admin_upload_resource_publishing_False( "view_resourcebase", "approve_resourcebase", "publish_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_manager: [], self.group_member: [], @@ -2098,6 +2118,8 @@ def test_permissions_are_set_as_expected_admin_upload_resource_publishing_False( "publish_resourcebase", "view_resourcebase", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_manager: ["view_resourcebase", "approve_resourcebase", "publish_resourcebase"], self.group_member: ["view_resourcebase"], @@ -2140,6 +2162,8 @@ def test_permissions_on_user_role_promotion_to_manager(self): "publish_resourcebase", "change_resourcebase_permissions", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_member: [ "change_resourcebase", @@ -2150,6 +2174,8 @@ def test_permissions_on_user_role_promotion_to_manager(self): "publish_resourcebase", "change_resourcebase_permissions", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], } try: @@ -2219,6 +2245,8 @@ def test_permissions_on_user_role_demote_to_member_only_RESOURCE_PUBLISHING_acti "change_resourcebase_metadata", "change_resourcebase_permissions", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_manager: ["download_resourcebase", "view_resourcebase"], self.group_member: ["download_resourcebase", "view_resourcebase"], @@ -2250,6 +2278,8 @@ def test_permissions_on_user_role_promote_to_manager_only_RESOURCE_PUBLISHING_ac "change_resourcebase_metadata", "change_resourcebase_permissions", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_manager: [ "change_resourcebase", @@ -2261,6 +2291,8 @@ def test_permissions_on_user_role_promote_to_manager_only_RESOURCE_PUBLISHING_ac "change_resourcebase_permissions", "approve_resourcebase", "publish_resourcebase", + "change_dataset_style", + "change_dataset_data", ], self.group_member: [ "change_resourcebase", @@ -2271,6 +2303,8 @@ def test_permissions_on_user_role_promote_to_manager_only_RESOURCE_PUBLISHING_ac "publish_resourcebase", "change_resourcebase_permissions", "approve_resourcebase", + "change_dataset_style", + "change_dataset_data", ], } for authorized_subject, expected_perms in expected.items(): From 5bcbd82c81268bbe53442ef713275858c130b651 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:09:47 +0200 Subject: [PATCH 07/61] [Fixes #12455] Map thumbnails extent always cover the whole globe (#12460) * [Fixes #12455] Map thumbnails extent always cover the whole globe * [Fixes #12455] Map thumbnails extent always cover the whole globe * [Fixes #12455] Map thumbnails extent always cover the whole globe * Fixed bbox calculation and repositioned utils * [Fixes #12455] fix test --------- Co-authored-by: G. Allegri --- geonode/base/bbox_utils.py | 98 +++++++++++++++++++++++++++++++ geonode/maps/models.py | 46 ++++++++++----- geonode/resource/utils.py | 8 ++- geonode/thumbs/tests/test_unit.py | 2 +- geonode/thumbs/thumbnails.py | 41 ++++--------- geonode/thumbs/utils.py | 89 ---------------------------- 6 files changed, 148 insertions(+), 136 deletions(-) diff --git a/geonode/base/bbox_utils.py b/geonode/base/bbox_utils.py index dc7ca8c4baf..b589e1b9f16 100644 --- a/geonode/base/bbox_utils.py +++ b/geonode/base/bbox_utils.py @@ -20,15 +20,24 @@ import math import copy import json +import re +import logging from decimal import Decimal from typing import Union, List, Generator + +from pyproj import CRS from shapely import affinity from shapely.ops import split from shapely.geometry import mapping, Polygon, LineString, GeometryCollection from django.contrib.gis.geos import Polygon as DjangoPolygon +from geonode import GeoNodeException +from geonode.utils import bbox_to_projection + +logger = logging.getLogger(__name__) + class BBOXHelper: """ @@ -228,3 +237,92 @@ def split_polygon( return GeometryCollection(geo_polygons) else: return geo_polygons + + +def transform_bbox(bbox: List, target_crs: str = "EPSG:3857"): + """ + Function transforming BBOX in dataset compliant format (xmin, xmax, ymin, ymax, 'EPSG:xxxx') to another CRS, + preserving overflow values. + """ + match = re.match(r"^(EPSG:)?(?P\d{4,6})$", str(target_crs)) + target_srid = int(match.group("srid")) if match else 4326 + return list(bbox_to_projection(bbox, target_srid=target_srid))[:-1] + [target_crs] + + +def epsg_3857_area_of_use(target_crs="EPSG:4326"): + """ + Shortcut function, returning area of use of EPSG:3857 (in EPSG:4326) in a dataset compliant BBOX + """ + epsg3857 = CRS.from_user_input("EPSG:3857") + area_of_use = [ + getattr(epsg3857.area_of_use, "west"), + getattr(epsg3857.area_of_use, "east"), + getattr(epsg3857.area_of_use, "south"), + getattr(epsg3857.area_of_use, "north"), + "EPSG:4326", + ] + if target_crs != "EPSG:4326": + return transform_bbox(area_of_use, target_crs) + return area_of_use + + +def crop_to_3857_area_of_use(bbox: List) -> List: + # perform the comparison in EPSG:4326 (the pivot for EPSG:3857) + bbox4326 = transform_bbox(bbox, target_crs="EPSG:4326") + + # get area of use of EPSG:3857 in EPSG:4326 + epsg3857_bounds_bbox = epsg_3857_area_of_use() + + bbox = [] + for coord, bound_coord in zip(bbox4326[:-1], epsg3857_bounds_bbox[:-1]): + if abs(coord) > abs(bound_coord): + logger.debug("Thumbnail generation: cropping BBOX's coord to EPSG:3857 area of use.") + bbox.append(bound_coord) + else: + bbox.append(coord) + + bbox.append("EPSG:4326") + + return bbox + + +def exceeds_epsg3857_area_of_use(bbox: List) -> bool: + """ + Function checking if a provided BBOX extends the are of use of EPSG:3857. Comparison is performed after casting + the BBOX to EPSG:4326 (pivot for EPSG:3857). + + :param bbox: a dataset compliant BBOX in a certain CRS, in (xmin, xmax, ymin, ymax, 'EPSG:xxxx') order + :returns: List of indicators whether BBOX's coord exceeds the area of use of EPSG:3857 + """ + + # perform the comparison in EPSG:4326 (the pivot for EPSG:3857) + bbox4326 = transform_bbox(bbox, target_crs="EPSG:4326") + + # get area of use of EPSG:3857 in EPSG:4326 + epsg3857_bounds_bbox = epsg_3857_area_of_use() + + exceeds = False + for coord, bound_coord in zip(bbox4326[:-1], epsg3857_bounds_bbox[:-1]): + if abs(coord) > abs(bound_coord): + exceeds = True + + return exceeds + + +def clean_bbox(bbox, target_crs): + # make sure BBOX is provided with the CRS in a correct format + source_crs = bbox[-1] + + srid_regex = re.match(r"EPSG:\d+", source_crs) + if not srid_regex: + logger.error(f"Thumbnail bbox is in a wrong format: {bbox}") + raise GeoNodeException("Wrong BBOX format") + + # for the EPSG:3857 (default thumb's CRS) - make sure received BBOX can be transformed to the target CRS; + # if it can't be (original coords are outside of the area of use of EPSG:3857), thumbnail generation with + # the provided bbox is impossible. + if target_crs == "EPSG:3857" and bbox[-1].upper() != "EPSG:3857": + bbox = crop_to_3857_area_of_use(bbox) + + bbox = transform_bbox(bbox, target_crs=target_crs) + return bbox diff --git a/geonode/maps/models.py b/geonode/maps/models.py index 32f9e60dd8e..bbc546865a0 100644 --- a/geonode/maps/models.py +++ b/geonode/maps/models.py @@ -26,7 +26,7 @@ from django.template.defaultfilters import slugify from django.urls import reverse from django.utils.translation import gettext_lazy as _ - +from geonode.base import bbox_utils from geonode import geoserver # noqa from geonode.base.models import ResourceBase, LinkedResource from geonode.client.hooks import hookset @@ -129,23 +129,37 @@ def get_absolute_url(self): def embed_url(self): return reverse("map_embed", kwargs={"mapid": self.pk}) - def get_bbox_from_datasets(self, layers): + def compute_bbox(self, target_crs="EPSG:3857"): """ - Calculate the bbox from a given list of Dataset objects - - bbox format: [xmin, xmax, ymin, ymax] + Compute bbox for maps by looping on all maplayers and getting the max + bbox of all the datasets """ - bbox = None - for layer in layers: - dataset_bbox = layer.bbox - if bbox is None: - bbox = list(dataset_bbox[0:4]) - else: - bbox[0] = min(bbox[0], dataset_bbox[0]) - bbox[1] = max(bbox[1], dataset_bbox[1]) - bbox[2] = min(bbox[2], dataset_bbox[2]) - bbox[3] = max(bbox[3], dataset_bbox[3]) - + bbox = bbox_utils.epsg_3857_area_of_use(target_crs="EPSG:3857") + for layer in self.maplayers.filter(visibility=True).order_by("order").iterator(): + dataset = layer.dataset + if dataset is not None: + if dataset.ll_bbox_polygon: + dataset_bbox = bbox_utils.clean_bbox(dataset.ll_bbox, target_crs) + elif ( + dataset.bbox[-1].upper() != "EPSG:3857" + and target_crs.upper() == "EPSG:3857" + and bbox_utils.exceeds_epsg3857_area_of_use(dataset.bbox) + ): + # handle exceeding the area of use of the default thumb's CRS + dataset_bbox = bbox_utils.transform_bbox( + bbox_utils.crop_to_3857_area_of_use(dataset.bbox), target_crs + ) + else: + dataset_bbox = bbox_utils.transform_bbox(dataset.bbox, target_crs) + + bbox = [ + max(bbox[0], dataset_bbox[0]), + min(bbox[1], dataset_bbox[1]), + max(bbox[2], dataset_bbox[2]), + min(bbox[3], dataset_bbox[3]), + ] + + self.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], target_crs) return bbox @property diff --git a/geonode/resource/utils.py b/geonode/resource/utils.py index 4edfd9ccd47..0fe66a01f82 100644 --- a/geonode/resource/utils.py +++ b/geonode/resource/utils.py @@ -45,7 +45,7 @@ HierarchicalKeyword, SpatialRepresentationType, ) - +from geonode.maps.models import Map from ..layers.models import Dataset from ..documents.models import Document from ..documents.enumerations import DOCUMENT_TYPE_MAP, DOCUMENT_MIMETYPE_MAP @@ -408,6 +408,12 @@ def metadata_post_save(instance, *args, **kwargs): instance.uuid = _uuid Dataset.objects.filter(id=instance.id).update(uuid=_uuid) + if isinstance(instance, Map): + """ + For maps, we can calculate the bbox based on the maplayers + """ + instance.compute_bbox() + # Set a default user for accountstream to work correctly. if instance.owner is None: instance.owner = get_valid_user() diff --git a/geonode/thumbs/tests/test_unit.py b/geonode/thumbs/tests/test_unit.py index e344c823092..5d37124fcd7 100644 --- a/geonode/thumbs/tests/test_unit.py +++ b/geonode/thumbs/tests/test_unit.py @@ -248,7 +248,7 @@ def test_datasets_locations_simple_map(self): ) def test_datasets_locations_simple_map_default_bbox(self): - expected_bbox = [-8238681.374829309, -8220320.783295829, 4969844.0930337105, 4984363.884452854, "EPSG:3857"] + expected_bbox = [-20037397.023298446, 20037397.023298446, -20048966.104014594, 20048966.104014594, "EPSG:3857"] dataset = Dataset.objects.get(title_en="theaters_nyc") map = Map.objects.get(title_en="theaters_nyc_map") diff --git a/geonode/thumbs/thumbnails.py b/geonode/thumbs/thumbnails.py index b6eccbcbe77..f5b03859c7f 100644 --- a/geonode/thumbs/thumbnails.py +++ b/geonode/thumbs/thumbnails.py @@ -34,6 +34,7 @@ from geonode.geoserver.helpers import ogc_server_settings from geonode.utils import get_dataset_name, get_dataset_workspace from geonode.thumbs import utils +from geonode.base import bbox_utils from geonode.thumbs.exceptions import ThumbnailError logger = logging.getLogger(__name__) @@ -110,10 +111,12 @@ def create_thumbnail( if isinstance(instance, Map): is_map_with_datasets = MapLayer.objects.filter(map=instance, local=True).exclude(dataset=None).exists() + if is_map_with_datasets: + compute_bbox_from_datasets = True if bbox: - bbox = utils.clean_bbox(bbox, target_crs) + bbox = bbox_utils.clean_bbox(bbox, target_crs) elif instance.ll_bbox_polygon: - bbox = utils.clean_bbox(instance.ll_bbox, target_crs) + bbox = bbox_utils.clean_bbox(instance.ll_bbox, target_crs) else: compute_bbox_from_datasets = True @@ -268,16 +271,16 @@ def _datasets_locations( locations.append([instance.ows_url or ogc_server_settings.LOCATION, [instance.alternate], []]) if compute_bbox: if instance.ll_bbox_polygon: - bbox = utils.clean_bbox(instance.ll_bbox, target_crs) + bbox = bbox_utils.clean_bbox(instance.ll_bbox, target_crs) elif ( instance.bbox[-1].upper() != "EPSG:3857" and target_crs.upper() == "EPSG:3857" - and utils.exceeds_epsg3857_area_of_use(instance.bbox) + and bbox_utils.exceeds_epsg3857_area_of_use(instance.bbox) ): # handle exceeding the area of use of the default thumb's CRS - bbox = utils.transform_bbox(utils.crop_to_3857_area_of_use(instance.bbox), target_crs) + bbox = bbox_utils.transform_bbox(bbox_utils.crop_to_3857_area_of_use(instance.bbox), target_crs) else: - bbox = utils.transform_bbox(instance.bbox, target_crs) + bbox = bbox_utils.transform_bbox(instance.bbox, target_crs) elif isinstance(instance, Map): for maplayer in instance.maplayers.filter(visibility=True).order_by("order").iterator(): if maplayer.dataset and maplayer.dataset.sourcetype == SOURCE_TYPE_REMOTE and not maplayer.dataset.ows_url: @@ -335,29 +338,9 @@ def _datasets_locations( ] ) - if compute_bbox: - if dataset.ll_bbox_polygon: - dataset_bbox = utils.clean_bbox(dataset.ll_bbox, target_crs) - elif ( - dataset.bbox[-1].upper() != "EPSG:3857" - and target_crs.upper() == "EPSG:3857" - and utils.exceeds_epsg3857_area_of_use(dataset.bbox) - ): - # handle exceeding the area of use of the default thumb's CRS - dataset_bbox = utils.transform_bbox(utils.crop_to_3857_area_of_use(dataset.bbox), target_crs) - else: - dataset_bbox = utils.transform_bbox(dataset.bbox, target_crs) - - if not bbox: - bbox = dataset_bbox - else: - # dataset's BBOX: (left, right, bottom, top) - bbox = [ - min(bbox[0], dataset_bbox[0]), - max(bbox[1], dataset_bbox[1]), - min(bbox[2], dataset_bbox[2]), - max(bbox[3], dataset_bbox[3]), - ] + if compute_bbox: + instance.compute_bbox(target_crs) + bbox = instance.bbox if bbox and len(bbox) < 5: bbox = list(bbox) + [target_crs] # convert bbox to list, if it's tuple diff --git a/geonode/thumbs/utils.py b/geonode/thumbs/utils.py index 9c07003b05f..fa360e3f50b 100644 --- a/geonode/thumbs/utils.py +++ b/geonode/thumbs/utils.py @@ -17,13 +17,11 @@ # ######################################################################### import os -import re import time import base64 import logging from PIL import Image, ImageOps -from pyproj import CRS from typing import List, Tuple, Callable, Union from uuid import uuid4 from urllib.parse import urlencode @@ -31,7 +29,6 @@ from django.conf import settings from django.contrib.auth import get_user_model -from geonode.utils import bbox_to_projection from geonode.base.auth import get_or_create_token from geonode.thumbs.exceptions import ThumbnailError from geonode.storage.manager import storage_manager @@ -72,16 +69,6 @@ def make_bbox_to_pixels_transf(src_bbox: Union[List, Tuple], dest_bbox: Union[Li ) -def transform_bbox(bbox: List, target_crs: str = "EPSG:3857"): - """ - Function transforming BBOX in dataset compliant format (xmin, xmax, ymin, ymax, 'EPSG:xxxx') to another CRS, - preserving overflow values. - """ - match = re.match(r"^(EPSG:)?(?P\d{4,6})$", str(target_crs)) - target_srid = int(match.group("srid")) if match else 4326 - return list(bbox_to_projection(bbox, target_srid=target_srid))[:-1] + [target_crs] - - def expand_bbox_to_ratio( bbox: List, target_width: int = settings.THUMBNAIL_SIZE["width"], @@ -438,82 +425,6 @@ def getmap( return u -def epsg_3857_area_of_use(): - """ - Shortcut function, returning area of use of EPSG:3857 (in EPSG:4326) in a dataset compliant BBOX - """ - epsg3857 = CRS.from_user_input("EPSG:3857") - return [ - getattr(epsg3857.area_of_use, "west"), - getattr(epsg3857.area_of_use, "east"), - getattr(epsg3857.area_of_use, "south"), - getattr(epsg3857.area_of_use, "north"), - "EPSG:4326", - ] - - -def crop_to_3857_area_of_use(bbox: List) -> List: - # perform the comparison in EPSG:4326 (the pivot for EPSG:3857) - bbox4326 = transform_bbox(bbox, target_crs="EPSG:4326") - - # get area of use of EPSG:3857 in EPSG:4326 - epsg3857_bounds_bbox = epsg_3857_area_of_use() - - bbox = [] - for coord, bound_coord in zip(bbox4326[:-1], epsg3857_bounds_bbox[:-1]): - if abs(coord) > abs(bound_coord): - logger.debug("Thumbnail generation: cropping BBOX's coord to EPSG:3857 area of use.") - bbox.append(bound_coord) - else: - bbox.append(coord) - - bbox.append("EPSG:4326") - - return bbox - - -def exceeds_epsg3857_area_of_use(bbox: List) -> bool: - """ - Function checking if a provided BBOX extends the are of use of EPSG:3857. Comparison is performed after casting - the BBOX to EPSG:4326 (pivot for EPSG:3857). - - :param bbox: a dataset compliant BBOX in a certain CRS, in (xmin, xmax, ymin, ymax, 'EPSG:xxxx') order - :returns: List of indicators whether BBOX's coord exceeds the area of use of EPSG:3857 - """ - - # perform the comparison in EPSG:4326 (the pivot for EPSG:3857) - bbox4326 = transform_bbox(bbox, target_crs="EPSG:4326") - - # get area of use of EPSG:3857 in EPSG:4326 - epsg3857_bounds_bbox = epsg_3857_area_of_use() - - exceeds = False - for coord, bound_coord in zip(bbox4326[:-1], epsg3857_bounds_bbox[:-1]): - if abs(coord) > abs(bound_coord): - exceeds = True - - return exceeds - - -def clean_bbox(bbox, target_crs): - # make sure BBOX is provided with the CRS in a correct format - source_crs = bbox[-1] - - srid_regex = re.match(r"EPSG:\d+", source_crs) - if not srid_regex: - logger.error(f"Thumbnail bbox is in a wrong format: {bbox}") - raise ThumbnailError("Wrong BBOX format") - - # for the EPSG:3857 (default thumb's CRS) - make sure received BBOX can be transformed to the target CRS; - # if it can't be (original coords are outside of the area of use of EPSG:3857), thumbnail generation with - # the provided bbox is impossible. - if target_crs == "EPSG:3857" and bbox[-1].upper() != "EPSG:3857": - bbox = crop_to_3857_area_of_use(bbox) - - bbox = transform_bbox(bbox, target_crs=target_crs) - return bbox - - def thumb_path(filename): """Return the complete path of the provided thumbnail file accessible via Django storage API""" From 397394127c95e93ab04dce20eb5f58d7ad16a5d4 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:52:15 +0200 Subject: [PATCH 08/61] =?UTF-8?q?[Fixes=20#12596]=20User=20with=20notifica?= =?UTF-8?q?tion=20disabled=20still=20receive=20the=20noti=E2=80=A6=20(#125?= =?UTF-8?q?97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Fixes #12596] User with notification disabled still receive the notifications * [Fixes #12596] User with notification disabled still receive the notifications --- geonode/notifications_helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geonode/notifications_helper.py b/geonode/notifications_helper.py index fa850f34a76..0ee0a1a1981 100644 --- a/geonode/notifications_helper.py +++ b/geonode/notifications_helper.py @@ -111,9 +111,9 @@ def get_notification_recipients(notice_type_label, exclude_user=None, resource=N """Get notification recipients""" if not has_notifications: return [] - recipients_ids = notifications.models.NoticeSetting.objects.filter(notice_type__label=notice_type_label).values( - "user" - ) + recipients_ids = notifications.models.NoticeSetting.objects.filter( + notice_type__label=notice_type_label, send=True + ).values("user") profiles = get_user_model().objects.filter(id__in=recipients_ids) exclude_users_ids = [] From 684227f576d6c910bca5e6fc794c60b053a1a4ce Mon Sep 17 00:00:00 2001 From: Sylvain POULAIN Date: Mon, 23 Sep 2024 11:51:54 +0400 Subject: [PATCH 09/61] [Fix #12589] Update bbox command for maps (#12590) * Update bbox command for maps --- .../management/commands/sync_geonode_maps.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/geonode/geoserver/management/commands/sync_geonode_maps.py b/geonode/geoserver/management/commands/sync_geonode_maps.py index f0e0175eed5..c85b715eca5 100644 --- a/geonode/geoserver/management/commands/sync_geonode_maps.py +++ b/geonode/geoserver/management/commands/sync_geonode_maps.py @@ -32,7 +32,9 @@ def sync_geonode_maps(ignore_errors, filter, username, removeduplicates, - updatethumbnails): + updatethumbnails, + updatebbox, + ): maps = Map.objects.all().order_by('title') if filter: maps = maps.filter(title__icontains=filter) @@ -52,6 +54,9 @@ def sync_geonode_maps(ignore_errors, # remove duplicates print("Removing duplicate links...") remove_duplicate_links(map) + if updatebbox: + print("Regenerating BBOX...") + map.compute_bbox() except (Exception, RuntimeError): map_errors.append(map.title) exception_type, error, traceback = sys.exc_info() @@ -106,11 +111,15 @@ def add_arguments(self, parser): dest="updatethumbnails", default=False, help="Update the map styles and thumbnails.") + parser.add_argument( + "--updatebbox", action="store_true", dest="updatebbox", default=False, help="Update the map BBOX." + ) def handle(self, **options): ignore_errors = options.get('ignore_errors') removeduplicates = options.get('removeduplicates') updatethumbnails = options.get('updatethumbnails') + updatebbox = options.get("updatebbox") filter = options.get('filter') if not options.get('username'): username = None @@ -121,4 +130,6 @@ def handle(self, **options): filter, username, removeduplicates, - updatethumbnails) + updatethumbnails, + updatebbox, + ) From 09ffac7c854d43d79a4f93e70862e4dd2242bae6 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:04:34 +0200 Subject: [PATCH 10/61] [Fixes #12326] Assets: implement migration for old uploaded files (#12411) * [Fixes #12124] GNIP 100: Assets (#12335) * [Fixes #12124] GNIP 100: Assets --------- Co-authored-by: etj * [Fixes #12226] Directory assets (#12337) [Fixes #12226] Directory assets --------- Co-authored-by: etj * [Fixes #12341] Asset download handler and link generator (#12343) * [Fixes #12341] Download handler fix * [Fixes #12341] Assets: link generation (#12342) --------- Co-authored-by: Emanuele Tajariol * [Fixes #12326] Assets: implement migration for old uploaded files * [Fixes #12326] Assets: implement migration for old uploaded files * [Fixes #12326] Assets: implement migration for old uploaded files * [Fixes #12326] rollback requirements --------- Co-authored-by: etj Co-authored-by: Emanuele Tajariol --- geonode/assets/management/__init__.py | 0 .../assets/management/commands/__init__.py | 0 .../commands/migrate_file_to_assets.py | 140 ++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 geonode/assets/management/__init__.py create mode 100644 geonode/assets/management/commands/__init__.py create mode 100644 geonode/assets/management/commands/migrate_file_to_assets.py diff --git a/geonode/assets/management/__init__.py b/geonode/assets/management/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/assets/management/commands/__init__.py b/geonode/assets/management/commands/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/assets/management/commands/migrate_file_to_assets.py b/geonode/assets/management/commands/migrate_file_to_assets.py new file mode 100644 index 00000000000..1b3297c18f0 --- /dev/null +++ b/geonode/assets/management/commands/migrate_file_to_assets.py @@ -0,0 +1,140 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +import sys +import shutil +import logging +from django.conf import settings +from geonode.assets.local import LocalAssetHandler +from geonode.assets.models import LocalAsset +from geonode.base.management.commands.helpers import confirm +from django.core.management.base import BaseCommand +from geonode.geoserver.helpers import gs_catalog + +from geonode.base.models import ResourceBase + +logger = logging.getLogger() +handler = logging.StreamHandler(sys.stdout) +logger.setLevel(logging.INFO) +formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +logger.addHandler(handler) + + +class Command(BaseCommand): + + help = """ + Migrate the files from MEDIA_ROOT to ASSETS_ROOT, update LocalAsset.location and GeoServer + REF: https://github.com/GeoNode/geonode/issues/12326 + """ + + def add_arguments(self, parser): + parser.add_argument( + '-n', + '--no-input', + dest='noinput', + default=False, + action='store_true', + help='Does not ask for confirmation for the run' + ) + parser.add_argument( + '-d', + '--dryrun', + dest='dryrun', + default=False, + action='store_true', + help='Perform a dryrun, will show the File to be moved, their new path and the file to be deleted' + ) + + def handle(self, **options): + question = "By running this command you are going to move all the files of your Resources into the ASSETS_ROOT. Do you want to continue?" + + if not options.get('noinput'): + result = confirm(question, resp=True) + if not result: + return + + dryrun = options.get('dryrun') + + handler = LocalAssetHandler() + + logger.info("Retrieving all assets with some files") + + for asset in LocalAsset.objects.iterator(): + logger.info(f"processing asset: {asset.title}") + + source = os.path.dirname(asset.location[0]) + + if dryrun: + logger.info(f"Files found: {asset.location}") + continue + + if settings.ASSETS_ROOT in source: + logger.info("The location is already the asset root, skipping...") + continue + + if not os.path.exists(source): + logger.warning("Source path of the file for Asset does not exists, skipping...") + continue + + try: + + logger.info("Moving file to the asset folder") + + dest = shutil.move(source, handler._create_asset_dir()) + + logger.info("Fixing perms") + if settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS is not None: + os.chmod(os.path.dirname(dest), settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS) + + if settings.FILE_UPLOAD_PERMISSIONS is not None: + os.chmod(dest, settings.FILE_UPLOAD_PERMISSIONS) + + except Exception as e: + logger.error(e) + continue + + logger.info("Updating location field with new folder value") + + asset.location = [x.replace(source, dest) for x in asset.location] + asset.save() + + logger.info("Checking if geoserver should be updated") + asset_obj = asset.link_set.values_list('resource', flat=True).first() + if not asset_obj: + logger.warning("No resources connected to the asset, skipping resource update...") + continue + resource = ResourceBase.objects.get(pk=asset_obj) + if resource.subtype == 'raster': + logger.info("Updating GeoServer value") + + store_to_update = gs_catalog.get_layer(resource.get_real_instance().alternate)\ + .resource\ + .store + + raster_file = [x for x in asset.location if os.path.basename(x).split('.')[-1] in ["tiff", "tif", "geotiff", "geotif"]] + store_to_update.url = f"file:{raster_file[0]}" + try: + gs_catalog.save(store_to_update) + except Exception: + logger.error(f"Error during GeoServer update for resource {resource}, please check GeoServer logs") + logger.info("Geoserver Updated") + + logger.info("Migration completed") From 852ce01422cca1cff33307c794483468547610f0 Mon Sep 17 00:00:00 2001 From: ahmdthr <116570171+ahmdthr@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:11:13 +0200 Subject: [PATCH 11/61] [Fixes #11703] Documents can now be uploaded without specifying title via the REST API. (#11872) * Documents can now be uploaded without specifying title via the REST API. * Added test for document upload without title. * Fixed the document upload without title test. * Fixed linting issues. * Fixed tests for uploading document without a title. * Fixed Documents serializer. * Added file name as title when no title is provided. * Fixed tests for uploading document without a title. * 1- Using the name of the file as a title for document upload if no title is specified instead of 'None'. 2- Fixed test for uploading document without a title. * Fixed the test for document upload without a title. --------- Co-authored-by: Giovanni Allegri --- geonode/documents/api/serializers.py | 2 ++ geonode/documents/api/tests.py | 13 +++++++++++++ geonode/documents/models.py | 4 ++-- geonode/resource/utils.py | 6 +++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/geonode/documents/api/serializers.py b/geonode/documents/api/serializers.py index 1fbcb8ed790..b0c63875060 100644 --- a/geonode/documents/api/serializers.py +++ b/geonode/documents/api/serializers.py @@ -19,6 +19,7 @@ import logging from dynamic_rest.fields.fields import DynamicComputedField +from rest_framework import serializers from geonode.base.api.serializers import ResourceBaseSerializer from geonode.documents.models import Document @@ -36,6 +37,7 @@ def get_attribute(self, instance): class DocumentSerializer(ResourceBaseSerializer): + title = serializers.CharField(required=False) file_path = GeonodeFilePathField(required=False, write_only=True) doc_file = DocumentFieldField(required=False, write_only=True) diff --git a/geonode/documents/api/tests.py b/geonode/documents/api/tests.py index f14b5ada5ef..4d144748d41 100644 --- a/geonode/documents/api/tests.py +++ b/geonode/documents/api/tests.py @@ -45,6 +45,7 @@ def setUp(self): self.url = reverse("documents-list") self.invalid_file_path = f"{settings.PROJECT_ROOT}/tests/data/thesaurus.rdf" self.valid_file_path = f"{settings.PROJECT_ROOT}/base/fixtures/test_xml.xml" + self.no_title_file_path = f"{settings.PROJECT_ROOT}/base/fixtures/test_sld.sld" def test_documents(self): """ @@ -176,6 +177,18 @@ def test_creation_should_create_the_doc(self): self.assertEqual("xml", extension) self.assertTrue(Document.objects.filter(title="New document for testing").exists()) + def test_uploading_doc_without_title(self): + """ + A document should be uploaded without specifying a title + """ + self.client.force_login(self.admin) + payload = {"document": {"metadata_only": True, "file_path": self.no_title_file_path}} + actual = self.client.post(self.url, data=payload, format="json") + self.assertEqual(201, actual.status_code) + extension = actual.json().get("document", {}).get("extension", "") + self.assertEqual("sld", extension) + self.assertTrue(Document.objects.filter(title="test_sld.sld").exists()) + def test_patch_point_of_contact(self): document = Document.objects.first() url = urljoin(f"{reverse('documents-list')}/", f"{document.id}") diff --git a/geonode/documents/models.py b/geonode/documents/models.py index cdd069929a5..626cccb5933 100644 --- a/geonode/documents/models.py +++ b/geonode/documents/models.py @@ -84,8 +84,8 @@ def files(self): @property def name(self): - if not self.title: - return str(self.id) + if not self.title and len(self.files) > 0: + return self.files[0].split("/")[-1] else: return self.title diff --git a/geonode/resource/utils.py b/geonode/resource/utils.py index 0fe66a01f82..84075dea14a 100644 --- a/geonode/resource/utils.py +++ b/geonode/resource/utils.py @@ -361,7 +361,11 @@ def document_post_save(instance, *args, **kwargs): url = instance.doc_url Document.objects.filter(id=instance.id).update( - extension=instance.extension, subtype=instance.subtype, doc_url=instance.doc_url, csw_type=instance.csw_type + title=instance.title, + extension=instance.extension, + subtype=instance.subtype, + doc_url=instance.doc_url, + csw_type=instance.csw_type, ) if name and url and ext: From 32f1cbc167000cafd7d8c303276779c217e9111a Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:09:19 +0200 Subject: [PATCH 12/61] [Fixes #12610] Metadata Wizard steps leaked inside the settings tab (#12613) --- geonode/documents/templates/layouts/doc_panels.html | 2 +- geonode/geoapps/templates/layouts/app_panels.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/geonode/documents/templates/layouts/doc_panels.html b/geonode/documents/templates/layouts/doc_panels.html index 10e26be6a8b..916847750ca 100644 --- a/geonode/documents/templates/layouts/doc_panels.html +++ b/geonode/documents/templates/layouts/doc_panels.html @@ -603,7 +603,7 @@ {% endblock extra_metadata_content %} {% endblock ownership %} - +
diff --git a/geonode/geoapps/templates/layouts/app_panels.html b/geonode/geoapps/templates/layouts/app_panels.html index 6f1fe4d803e..23ff9b5b106 100644 --- a/geonode/geoapps/templates/layouts/app_panels.html +++ b/geonode/geoapps/templates/layouts/app_panels.html @@ -548,6 +548,7 @@ {% block extra_metadata_content %} {% endblock %}
+
From 30b84a21fbba0f310c82b2f4ea28462cc875cefe Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Thu, 26 Sep 2024 18:16:38 +0200 Subject: [PATCH 13/61] [Fixes #12594] Error when saving a new map (#12595) * Fix maps creation issue * Fix maps creation issue * [Fixes #12594] Error when saing a new map * [Fixes #12594] Error when saing a new map * [Fixes #12594] Error when saing a new map * [Fixes #12594] Error when saing a new map * [Fixes #12594] Error when saing a new map * [Fixes #12594] Error when saing a new map --- geonode/base/api/serializers.py | 35 +++++++++++++++------------------ geonode/base/api/tests.py | 15 -------------- geonode/base/models.py | 3 +++ geonode/maps/api/tests.py | 25 +++++++++++++++++++++++ geonode/people/models.py | 9 +++++++++ geonode/security/utils.py | 2 +- 6 files changed, 54 insertions(+), 35 deletions(-) diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 9a5ad98b09f..ac136d33bab 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -552,22 +552,6 @@ def to_representation(self, instance): return ret -class ResourceManagementField(serializers.BooleanField): - MAPPING = {"is_approved": "can_approve", "is_published": "can_publish", "featured": "can_feature"} - - def to_internal_value(self, data): - new_val = super().to_internal_value(data) - user = self.context["request"].user - user_action = self.MAPPING.get(self.field_name) - instance = self.root.instance or ResourceBase.objects.get(pk=self.root.initial_data["pk"]) - if getattr(user, user_action)(instance): - logger.debug("User can perform the action, the new value is returned") - return new_val - else: - logger.warning(f"The user does not have the perms to update the value of {self.field_name}") - return getattr(instance, self.field_name) - - class ResourceBaseSerializer(DynamicModelSerializer): pk = serializers.CharField(read_only=True) uuid = serializers.CharField(read_only=True) @@ -608,10 +592,10 @@ class ResourceBaseSerializer(DynamicModelSerializer): popular_count = serializers.CharField(required=False) share_count = serializers.CharField(required=False) rating = serializers.CharField(required=False) - featured = ResourceManagementField(required=False) + featured = serializers.BooleanField(required=False) advertised = serializers.BooleanField(required=False) - is_published = ResourceManagementField(required=False) - is_approved = ResourceManagementField(required=False) + is_published = serializers.BooleanField(required=False) + is_approved = serializers.BooleanField(required=False) detail_url = DetailUrlField(read_only=True) created = serializers.DateTimeField(read_only=True) last_updated = serializers.DateTimeField(read_only=True) @@ -751,6 +735,13 @@ def to_internal_value(self, data): data = super(ResourceBaseSerializer, self).to_internal_value(data) return data + def update(self, instance, validated_data): + user = self.context["request"].user + for field in instance.ROLE_BASED_MANAGED_FIELDS: + if not user.can_change_resource_field(instance, field) and field in validated_data: + validated_data.pop(field) + return super().update(instance, validated_data) + def save(self, **kwargs): extent = self.validated_data.pop("extent", None) instance = super().save(**kwargs) @@ -767,6 +758,12 @@ def save(self, **kwargs): logger.exception(e) raise InvalidResourceException("The standard bbox provided is invalid") instance.set_bbox_polygon(coords, srid) + + user = self.context["request"].user + for field in instance.ROLE_BASED_MANAGED_FIELDS: + if not user.can_change_resource_field(instance, field): + logger.debug("User can perform the action, the default value is set") + setattr(user, field, getattr(ResourceBase, field).field.default) return instance diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index b039a013eab..2c839503d74 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -826,21 +826,6 @@ def test_resource_settings_field(self): self.assertIsNotNone(field) self.assertTrue(field.to_internal_value(True)) - def test_resource_settings_field_non_admin(self): - """ - Non-Admin is not able to change the is_published value - if he is not the owner of the resource - """ - doc = create_single_doc("my_custom_doc") - factory = RequestFactory() - rq = factory.get("test") - rq.user = get_user_model().objects.get(username="bobby") - serializer = ResourceBaseSerializer(doc, context={"request": rq}) - field = serializer.fields["is_published"] - self.assertIsNotNone(field) - # the original value was true, so it should not return false - self.assertTrue(field.to_internal_value(False)) - def test_delete_user_with_resource(self): owner, created = get_user_model().objects.get_or_create(username="delet-owner") Dataset( diff --git a/geonode/base/models.py b/geonode/base/models.py index b22de4bf0a9..00297dca75b 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -632,6 +632,9 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): Base Resource Object loosely based on ISO 19115:2003 """ + # fixing up the publishing option based on user permissions + ROLE_BASED_MANAGED_FIELDS = ["is_approved", "is_published", "featured"] + BASE_PERMISSIONS = { "read": ["view_resourcebase"], "write": ["change_resourcebase_metadata"], diff --git a/geonode/maps/api/tests.py b/geonode/maps/api/tests.py index ca4ab144877..32f020bda34 100644 --- a/geonode/maps/api/tests.py +++ b/geonode/maps/api/tests.py @@ -270,6 +270,31 @@ def test_create_map(self): self.assertIsNotNone(response_maplayer["dataset"]) self.assertIsNotNone(response.data["map"]["thumbnail_url"]) + def test_create_map_featured_status_admin(self): + """ + Post to maps/ + User with perms should be able to change the value in the post payload + """ + # Get Layers List (backgrounds) + url = reverse("maps-list") + + data = { + "title": "Map should be approved", + "featured": True, + "is_approved": False, + "is_published": False, + "data": DUMMY_MAPDATA, + "maplayers": DUMMY_MAPLAYERS_DATA, + } + # if has perms, the user should be able to change the field + # featured/approved/published + self.client.login(username="admin", password="admin") + response = self.client.post(f"{url}?include[]=data", data=data, format="json") + self.assertEqual(response.status_code, 201) + self.assertFalse(response.json()["map"]["is_published"]) + self.assertFalse(response.json()["map"]["is_approved"]) + self.assertTrue(response.json()["map"]["featured"]) + def test_create_map_with_extra_maplayer_info(self): """ Post to maps/ diff --git a/geonode/people/models.py b/geonode/people/models.py index b3c3804756c..d115d2c85f3 100644 --- a/geonode/people/models.py +++ b/geonode/people/models.py @@ -261,6 +261,15 @@ def send_mail(self, template_prefix, context): if self.email: get_adapter().send_mail(template_prefix, self.email, context) + def can_change_resource_field(self, resource, field): + match field: + case "is_approved": + return self.can_approve(resource) + case "is_published": + return self.can_publish(resource) + case "featured": + return self.can_feature(resource) + def can_approve(self, resource): return can_approve(self, resource) diff --git a/geonode/security/utils.py b/geonode/security/utils.py index b163e036806..f39b0b28dd3 100644 --- a/geonode/security/utils.py +++ b/geonode/security/utils.py @@ -783,6 +783,6 @@ def can_publish(user, resource): if is_superuser: return True elif AdvancedSecurityWorkflowManager.is_manager_publish_mode(): - return is_manager and can_publish + return is_manager else: return is_owner or is_manager From 33be915e234dbbf0228ebe7639f9e73b161c12a8 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:24:34 +0200 Subject: [PATCH 14/61] [Fixes #12627] Include assets inside B/R (#12628) * [Fixes #12627] Include assets inside B/R * [Fixes #12627] force localasset in get_link_url * [Fixes #12627] fix pr comments * [Fixes #12627] add todo for assets_root replace * [Fixes #12627] change utils assets_root name --- geonode/assets/handlers.py | 1 + geonode/assets/local.py | 16 +++++++++ geonode/assets/models.py | 14 +++++++- geonode/assets/tests.py | 10 +++--- geonode/assets/utils.py | 5 +-- geonode/br/management/commands/backup.py | 36 ++++++++++++------- geonode/br/management/commands/restore.py | 28 ++++++++++----- .../commands/settings_docker_sample.ini | 4 +-- .../management/commands/settings_sample.ini | 4 +-- geonode/br/management/commands/utils/utils.py | 1 + geonode/documents/models.py | 4 +-- 11 files changed, 88 insertions(+), 35 deletions(-) diff --git a/geonode/assets/handlers.py b/geonode/assets/handlers.py index 3df019c521c..2e728ea3375 100644 --- a/geonode/assets/handlers.py +++ b/geonode/assets/handlers.py @@ -78,6 +78,7 @@ def get_default_handler(self) -> AssetHandlerInterface: return self._default_handler def get_handler(self, asset): + asset = asset.get_real_instance() if isinstance(asset, Asset) else asset asset_cls = asset if isinstance(asset, type) else asset.__class__ ret = self._registry.get(asset_cls, None) if not ret: diff --git a/geonode/assets/local.py b/geonode/assets/local.py index 558a22cecc4..aa8e486c485 100644 --- a/geonode/assets/local.py +++ b/geonode/assets/local.py @@ -28,6 +28,9 @@ def get_link_url(self, asset: LocalAsset): class IndexLocalLinkUrlHandler: def get_link_url(self, asset: LocalAsset): + asset = asset.get_real_instance() + if not isinstance(asset, LocalAsset): + raise TypeError("Only localasset are allowed") return build_absolute_uri(reverse("assets-link", args=(asset.pk,))) + f"/{os.path.basename(asset.location[0])}" @@ -76,6 +79,7 @@ def remove_data(self, asset: LocalAsset): Removes the files related to an Asset. Only files within the Assets directory are removed """ + asset = self.__force_real_instance(asset) if self._are_files_managed(asset): logger.info(f"Removing files for asset {asset.pk}") base = self._get_managed_dir(asset) @@ -85,6 +89,7 @@ def remove_data(self, asset: LocalAsset): logger.info(f"Not removing unmanaged files for asset {asset.pk}") def replace_data(self, asset: LocalAsset, files: list): + asset = self.__force_real_instance(asset) self.remove_data(asset) asset.location = files asset.save() @@ -123,6 +128,7 @@ def _clone_data(self, source_dir): def clone(self, source: LocalAsset) -> LocalAsset: # get a new asset instance to be edited and stored back + source = self.__force_real_instance(source) asset = LocalAsset.objects.get(pk=source.pk) # only copy files if they are managed @@ -204,12 +210,22 @@ def _get_managed_dir(cls, asset): return managed_dir + @classmethod + def __force_real_instance(cls, asset): + asset = asset.get_real_instance() + if not isinstance(asset, LocalAsset): + raise TypeError(f"Real instance of asset {asset} is not {cls.handled_asset_class()}") + return asset + class LocalAssetDownloadHandler(AssetDownloadHandlerInterface): def create_response( self, asset: LocalAsset, attachment: bool = False, basename: str = None, path: str = None ) -> HttpResponse: + asset = asset.get_real_instance() + if not isinstance(asset, LocalAsset): + raise TypeError("Only localasset are allowed") if not asset.location: return HttpResponse("Asset does not contain any data", status=500) diff --git a/geonode/assets/models.py b/geonode/assets/models.py index a4fe3a26de6..2d817913037 100644 --- a/geonode/assets/models.py +++ b/geonode/assets/models.py @@ -5,6 +5,18 @@ from django.contrib.auth import get_user_model +class AssetPolymorphicManager(PolymorphicManager): + """ + This override is required for the dump procedure. + Otherwise django is not able to dump the base objects + and will be upcasted to polymorphic models + https://github.com/jazzband/django-polymorphic/blob/cfd49b26d580d99b00dcd43a02409ce439a2c78f/polymorphic/base.py#L161-L175 + """ + + def get_queryset(self): + return super().get_queryset().non_polymorphic() + + class Asset(PolymorphicModel): """ A generic data linked to a ResourceBase @@ -16,7 +28,7 @@ class Asset(PolymorphicModel): owner = models.ForeignKey(get_user_model(), null=False, blank=False, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) - objects = PolymorphicManager() + objects = AssetPolymorphicManager() class Meta: verbose_name_plural = "Assets" diff --git a/geonode/assets/tests.py b/geonode/assets/tests.py index 8efb66ac181..0401a7a0943 100644 --- a/geonode/assets/tests.py +++ b/geonode/assets/tests.py @@ -71,7 +71,7 @@ def test_creation_and_delete_data_cloned(self): asset.save() self.assertIsInstance(asset, LocalAsset) - reloaded = Asset.objects.get(pk=asset.pk) + reloaded = LocalAsset.objects.get(pk=asset.pk) self.assertIsNotNone(reloaded) self.assertIsInstance(reloaded, LocalAsset) file = reloaded.location[0] @@ -103,7 +103,7 @@ def test_creation_and_delete_data_external(self): asset.save() self.assertIsInstance(asset, LocalAsset) - reloaded = Asset.objects.get(pk=asset.pk) + reloaded = LocalAsset.objects.get(pk=asset.pk) self.assertIsNotNone(reloaded) self.assertIsInstance(reloaded, LocalAsset) file = reloaded.location[0] @@ -128,7 +128,7 @@ def test_clone_and_delete_data_managed(self): asset.save() self.assertIsInstance(asset, LocalAsset) - reloaded = Asset.objects.get(pk=asset.pk) + reloaded = LocalAsset.objects.get(pk=asset.pk) cloned = asset_handler.clone(reloaded) self.assertNotEqual(reloaded.pk, cloned.pk) @@ -161,7 +161,7 @@ def test_clone_and_delete_data_unmanaged(self): asset.save() self.assertIsInstance(asset, LocalAsset) - reloaded = Asset.objects.get(pk=asset.pk) + reloaded = LocalAsset.objects.get(pk=asset.pk) cloned = asset_handler.clone(reloaded) self.assertEqual(reloaded.location[0], cloned.location[0]) @@ -268,7 +268,7 @@ def _setup_test(self, u, _file=ONE_JSON): asset.save() self.assertIsInstance(asset, LocalAsset) - reloaded = Asset.objects.get(pk=asset.pk) + reloaded = LocalAsset.objects.get(pk=asset.pk) # put two more files in the asset dir asset_dir = os.path.dirname(reloaded.location[0]) diff --git a/geonode/assets/utils.py b/geonode/assets/utils.py index 06abd560ea0..e3a26a806e6 100644 --- a/geonode/assets/utils.py +++ b/geonode/assets/utils.py @@ -43,8 +43,8 @@ def get_default_asset(resource: ResourceBase, link_type=None) -> Asset or None: filters = {"link__resource": resource} if link_type: filters["link__link_type"] = link_type - - return Asset.objects.filter(**filters).first() + asset = Asset.objects.filter(**filters).first() + return asset.get_real_instance() if asset else None DEFAULT_TYPES = {"image": ["jpg", "jpeg", "gif", "png", "bmp", "svg"]} @@ -55,6 +55,7 @@ def find_type(ext): def create_link(resource, asset, link_type=None, extension=None, name=None, mime=None, asset_handler=None, **kwargs): + asset = asset.get_real_instance() asset_handler = asset_handler or asset_handler_registry.get_handler(asset) if not link_type or not extension or not name: diff --git a/geonode/br/management/commands/backup.py b/geonode/br/management/commands/backup.py index 1b23dde6915..c54c040cbc0 100644 --- a/geonode/br/management/commands/backup.py +++ b/geonode/br/management/commands/backup.py @@ -25,6 +25,9 @@ import re import logging +from geonode.assets.local import LocalAssetHandler +from geonode.assets.models import LocalAsset + from .utils import utils from requests.auth import HTTPBasicAuth @@ -166,23 +169,21 @@ def execute_backup(self, **options): logger.info(f" - Dumping '{app_name}' into '{dump_name}.json'") # Point stdout at a file for dumping data to. - with open(os.path.join(fixtures_target, f"{dump_name}.json"), "w") as output: - call_command("dumpdata", app_name, format="json", indent=2, stdout=output) + output_file = os.path.join(fixtures_target, f"{dump_name}.json") + call_command("dumpdata", app_name, output=output_file) # Store Media Root - logger.info("*** Dumping GeoNode media folder...") - media_root = settings.MEDIA_ROOT media_folder = os.path.join(target_folder, utils.MEDIA_ROOT) - if not os.path.exists(media_folder): - os.makedirs(media_folder, exist_ok=True) + logger.info("*** Dumping GeoNode media folder...") + self.backup_folder(folder=media_folder, root=settings.MEDIA_ROOT, config=config) - copy_tree( - media_root, - media_folder, - ignore=utils.ignore_time(config.gs_data_dt_filter[0], config.gs_data_dt_filter[1]), - ) - logger.info(f"Saved media files from '{media_root}'") + logger.info("*** Dumping GeoNode assets folder...") + assets_folder = os.path.join(target_folder, utils.ASSETS_ROOT) + self.backup_folder(folder=assets_folder, root=settings.ASSETS_ROOT, config=config) + for instance in LocalAsset.objects.iterator(): + if not LocalAssetHandler._are_files_managed(instance): + logger.warning(f"The file for the asset with id {instance.pk} were not backup since is not managed by GeoNode") # Create Final ZIP Archive logger.info("*** Creating final ZIP archive...") @@ -214,6 +215,17 @@ def execute_backup(self, **options): return str(os.path.join(backup_dir, f"{dir_time_suffix}.zip")) + def backup_folder(self, folder, root, config): + if not os.path.exists(folder): + os.makedirs(root, exist_ok=True) + + copy_tree( + root, + folder, + ignore=utils.ignore_time(config.gs_data_dt_filter[0], config.gs_data_dt_filter[1]), + ) + logger.info(f"Saved files from '{root}'") + def create_geoserver_backup(self, config, settings, target_folder, ignore_errors): # Create GeoServer Backup url = settings.OGC_SERVER["default"]["LOCATION"] diff --git a/geonode/br/management/commands/restore.py b/geonode/br/management/commands/restore.py index e8b48ba8287..012122ac83a 100755 --- a/geonode/br/management/commands/restore.py +++ b/geonode/br/management/commands/restore.py @@ -265,13 +265,16 @@ def execute_restore(self, **options): # Write Checks media_root = settings.MEDIA_ROOT media_folder = os.path.join(target_folder, utils.MEDIA_ROOT) - + assets_root = settings.ASSETS_ROOT + assets_folder = os.path.join(target_folder, utils.ASSETS_ROOT) try: logger.info("*** Performing some checks...") logger.info(f"[Sanity Check] Full Write Access to restore folder: '{restore_folder}' ...") chmod_tree(restore_folder) logger.info(f"[Sanity Check] Full Write Access to media root: '{media_root}' ...") chmod_tree(media_root) + logger.info(f"[Sanity Check] Full Write Access to assets root: '{assets_root}' ...") + chmod_tree(assets_root) except Exception as e: if notify: restore_notification.apply_async( @@ -393,15 +396,11 @@ def execute_restore(self, **options): # Restore Media Root logger.info("*** Restore media root...") - if config.gs_data_dt_filter[0] is None: - shutil.rmtree(media_root, ignore_errors=True) - - if not os.path.exists(media_root): - os.makedirs(media_root, exist_ok=True) + self.restore_folder(config, media_root, media_folder) + logger.info("*** Restore assets root...") + self.restore_folder(config, assets_root, assets_folder) - copy_tree(media_folder, media_root) - chmod_tree(media_root) - logger.info(f"Media files restored into '{media_root}'.") + # TODO improve this part, by saving the original asset_root path in a variable, then replace with the new one # store backup info restored_backup = RestoredBackup( @@ -441,6 +440,17 @@ def execute_restore(self, **options): logger.info("*** Final filesystem cleanup ...") shutil.rmtree(restore_folder) + def restore_folder(self, config, root, folder): + if config.gs_data_dt_filter[0] is None: + shutil.rmtree(root, ignore_errors=True) + + if not os.path.exists(root): + os.makedirs(root, exist_ok=True) + + copy_tree(folder, root) + chmod_tree(root) + logger.info(f"Files restored into '{root}'.") + def validate_backup_file_options(self, **options) -> None: """ Method validating --backup-file and --backup-files-dir options diff --git a/geonode/br/management/commands/settings_docker_sample.ini b/geonode/br/management/commands/settings_docker_sample.ini index 68bbfdf4eff..f5401a9c8fa 100644 --- a/geonode/br/management/commands/settings_docker_sample.ini +++ b/geonode/br/management/commands/settings_docker_sample.ini @@ -13,6 +13,6 @@ dumprasterdata = yes # data_layername_exclude_filter = {comma separated list of layernames, optionally with glob syntax} e.g.: tuscany_*,italy [fixtures] -apps = contenttypes,auth,people,groups,account,guardian,admin,actstream,announcements,avatar,base,documents,geoserver,invitations,pinax_notifications,harvesting,services,layers,maps,oauth2_provider,sites,socialaccount,taggit,tastypie,upload,user_messages,geonode_themes,geoapps,favorite,geonode_client -dumps = contenttypes,auth,people,groups,account,guardian,admin,actstream,announcements,avatar,base,documents,geoserver,invitations,pinax_notifications,harvesting,services,layers,maps,oauth2_provider,sites,socialaccount,taggit,tastypie,upload,user_messages,geonode_themes,geoapps,favorite,geonode_client +apps = contenttypes,auth,people,groups,account,guardian,admin,actstream,announcements,avatar,assets,base,documents,geoserver,invitations,pinax_notifications,harvesting,services,layers,maps,oauth2_provider,sites,socialaccount,taggit,tastypie,upload,user_messages,geonode_themes,geoapps,favorite,geonode_client +dumps = contenttypes,auth,people,groups,account,guardian,admin,actstream,announcements,avatar,assets,base,documents,geoserver,invitations,pinax_notifications,harvesting,services,layers,maps,oauth2_provider,sites,socialaccount,taggit,tastypie,upload,user_messages,geonode_themes,geoapps,favorite,geonode_client diff --git a/geonode/br/management/commands/settings_sample.ini b/geonode/br/management/commands/settings_sample.ini index 157414ab5e0..112b6618474 100644 --- a/geonode/br/management/commands/settings_sample.ini +++ b/geonode/br/management/commands/settings_sample.ini @@ -13,5 +13,5 @@ dumprasterdata = yes # data_layername_exclude_filter = {comma separated list of layernames, optionally with glob syntax} e.g.: tuscany_*,italy [fixtures] -apps = contenttypes,auth,people,groups,account,guardian,admin,actstream,announcements,avatar,base,documents,geoserver,invitations,pinax_notifications,harvesting,services,layers,maps,oauth2_provider,sites,socialaccount,taggit,tastypie,upload,user_messages,geonode_themes,geoapps,favorite,geonode_client -dumps = contenttypes,auth,people,groups,account,guardian,admin,actstream,announcements,avatar,base,documents,geoserver,invitations,pinax_notifications,harvesting,services,layers,maps,oauth2_provider,sites,socialaccount,taggit,tastypie,upload,user_messages,geonode_themes,geoapps,favorite,geonode_client +apps = contenttypes,auth,people,groups,account,guardian,admin,actstream,announcements,avatar,assets,base,documents,geoserver,invitations,pinax_notifications,harvesting,services,layers,maps,oauth2_provider,sites,socialaccount,taggit,tastypie,upload,user_messages,geonode_themes,geoapps,favorite,geonode_client +dumps = contenttypes,auth,people,groups,account,guardian,admin,actstream,announcements,avatar,assets,base,documents,geoserver,invitations,pinax_notifications,harvesting,services,layers,maps,oauth2_provider,sites,socialaccount,taggit,tastypie,upload,user_messages,geonode_themes,geoapps,favorite,geonode_client diff --git a/geonode/br/management/commands/utils/utils.py b/geonode/br/management/commands/utils/utils.py index cb313193554..4b4839973ba 100644 --- a/geonode/br/management/commands/utils/utils.py +++ b/geonode/br/management/commands/utils/utils.py @@ -42,6 +42,7 @@ TEMPLATE_DIRS = "template_dirs" LOCALE_PATHS = "locale_dirs" EXTERNAL_ROOT = "external" +ASSETS_ROOT = "assets" logger = logging.getLogger(__name__) diff --git a/geonode/documents/models.py b/geonode/documents/models.py index 626cccb5933..565eb32122f 100644 --- a/geonode/documents/models.py +++ b/geonode/documents/models.py @@ -26,7 +26,7 @@ from django.utils.functional import classproperty from django.utils.translation import gettext_lazy as _ -from geonode.assets.models import Asset +from geonode.assets.models import LocalAsset from geonode.client.hooks import hookset from geonode.base.models import ResourceBase from geonode.groups.conf import settings as groups_settings @@ -79,7 +79,7 @@ def compact_permission_labels(cls): @property def files(self): - asset = Asset.objects.filter(link__resource=self).first() + asset = LocalAsset.objects.filter(link__resource=self).first() return asset.location if asset else [] @property From 677c5783112b92581177237f5412e5a2d4263176 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Mon, 7 Oct 2024 18:29:17 +0200 Subject: [PATCH 15/61] Fix bbox comparison during map extent calculation (#12637) --- geonode/maps/models.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/geonode/maps/models.py b/geonode/maps/models.py index bbc546865a0..ee0982f2d46 100644 --- a/geonode/maps/models.py +++ b/geonode/maps/models.py @@ -18,7 +18,7 @@ ######################################################################### import json import logging - +import math import itertools from deprecated import deprecated @@ -134,30 +134,36 @@ def compute_bbox(self, target_crs="EPSG:3857"): Compute bbox for maps by looping on all maplayers and getting the max bbox of all the datasets """ - bbox = bbox_utils.epsg_3857_area_of_use(target_crs="EPSG:3857") + epsg_bbox = bbox_utils.epsg_3857_area_of_use(target_crs="EPSG:3857") + bbox = [math.inf, -math.inf, math.inf, -math.inf] + for layer in self.maplayers.filter(visibility=True).order_by("order").iterator(): dataset = layer.dataset if dataset is not None: if dataset.ll_bbox_polygon: dataset_bbox = bbox_utils.clean_bbox(dataset.ll_bbox, target_crs) - elif ( - dataset.bbox[-1].upper() != "EPSG:3857" - and target_crs.upper() == "EPSG:3857" - and bbox_utils.exceeds_epsg3857_area_of_use(dataset.bbox) - ): + elif dataset.bbox: # handle exceeding the area of use of the default thumb's CRS dataset_bbox = bbox_utils.transform_bbox( bbox_utils.crop_to_3857_area_of_use(dataset.bbox), target_crs ) - else: - dataset_bbox = bbox_utils.transform_bbox(dataset.bbox, target_crs) - - bbox = [ - max(bbox[0], dataset_bbox[0]), - min(bbox[1], dataset_bbox[1]), - max(bbox[2], dataset_bbox[2]), - min(bbox[3], dataset_bbox[3]), - ] + + if dataset_bbox: + bbox = [ + min(bbox[0], dataset_bbox[0]), + max(bbox[1], dataset_bbox[1]), + min(bbox[2], dataset_bbox[2]), + max(bbox[3], dataset_bbox[3]), + ] + + if bbox[0] < epsg_bbox[0]: + bbox[0] = epsg_bbox[0] + if bbox[1] > epsg_bbox[1]: + bbox[1] = epsg_bbox[1] + if bbox[2] < epsg_bbox[2]: + bbox[2] = epsg_bbox[2] + if bbox[3] > epsg_bbox[3]: + bbox[3] = epsg_bbox[3] self.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], target_crs) return bbox From e54b42cf6a12f34c1d00a98d5332bcb66f8e9538 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Tue, 15 Oct 2024 12:12:18 +0200 Subject: [PATCH 16/61] [Fixes #12651] Wrong extent calculated for empty maps (#12652) * [Fixes #12651] Wrong extent calculated for empty maps * use iterator to iterate map layers --- geonode/maps/models.py | 13 ++++--------- geonode/maps/utils/__init__.py | 18 ------------------ 2 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 geonode/maps/utils/__init__.py diff --git a/geonode/maps/models.py b/geonode/maps/models.py index ee0982f2d46..7536d017297 100644 --- a/geonode/maps/models.py +++ b/geonode/maps/models.py @@ -135,8 +135,8 @@ def compute_bbox(self, target_crs="EPSG:3857"): bbox of all the datasets """ epsg_bbox = bbox_utils.epsg_3857_area_of_use(target_crs="EPSG:3857") - bbox = [math.inf, -math.inf, math.inf, -math.inf] + bbox = [math.inf, -math.inf, math.inf, -math.inf] for layer in self.maplayers.filter(visibility=True).order_by("order").iterator(): dataset = layer.dataset if dataset is not None: @@ -156,14 +156,9 @@ def compute_bbox(self, target_crs="EPSG:3857"): max(bbox[3], dataset_bbox[3]), ] - if bbox[0] < epsg_bbox[0]: - bbox[0] = epsg_bbox[0] - if bbox[1] > epsg_bbox[1]: - bbox[1] = epsg_bbox[1] - if bbox[2] < epsg_bbox[2]: - bbox[2] = epsg_bbox[2] - if bbox[3] > epsg_bbox[3]: - bbox[3] = epsg_bbox[3] + # if the starting bbox is not mutated it means no bbox has been computed from layers + if bbox[0] == math.inf: + bbox = epsg_bbox self.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], target_crs) return bbox diff --git a/geonode/maps/utils/__init__.py b/geonode/maps/utils/__init__.py deleted file mode 100644 index 6b4db6084e8..00000000000 --- a/geonode/maps/utils/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -######################################################################### -# -# Copyright (C) 2021 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 b6a26f38c7c2114bafa66f37aa77e01e4f7e2b95 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:07:01 +0200 Subject: [PATCH 17/61] [Fixes #12368] Introduce geonode-importer in geonode-core (#12570) * [Fixes #12368] Test Include Importer in GeoNode * [Fixes #12368] Removed unused function/deadcode/files * [Fixes #12368] Fix dataset assign for sld and xml * [Fixes #12368] add pre-validation step * [Fixes #12368] fix serializers for overwrite workflow * [Fixes #12368] fix replace * [Fixes #12368] Fix sld serializer --- .circleci/config.yml | 11 +- .env_test | 6 +- docker-compose-test.yml | 18 +- geonode/__init__.py | 2 +- geonode/assets/tests.py | 21 +- geonode/base/__init__.py | 6 - geonode/base/api/tests.py | 16 +- geonode/base/auth.py | 17 - geonode/base/bbox_utils.py | 7 - geonode/base/forms.py | 17 - geonode/base/models.py | 9 - geonode/catalogue/views.py | 10 - geonode/client/conf.py | 8 - geonode/decorators.py | 41 - geonode/documents/forms.py | 6 - geonode/favorite/utils.py | 44 - geonode/geoserver/helpers.py | 164 +-- geonode/geoserver/manager.py | 278 ------ geonode/geoserver/tests/test_manager.py | 97 -- geonode/geoserver/upload.py | 263 ----- geonode/harvesting/harvesters/base.py | 24 +- .../harvesting/harvesters/geonodeharvester.py | 127 +-- geonode/harvesting/resourcedescriptor.py | 5 - geonode/harvesting/tasks.py | 4 - .../test_harvester_worker_geonode_legacy.py | 2 +- geonode/layers/api/exceptions.py | 7 - geonode/layers/forms.py | 9 - geonode/layers/utils.py | 85 -- geonode/layers/views.py | 25 - geonode/maps/api/serializers.py | 11 +- geonode/maps/views.py | 12 - geonode/messaging/consumer.py | 29 - geonode/messaging/producer.py | 16 +- geonode/monitoring/service_handlers.py | 4 - geonode/notifications_helper.py | 9 - geonode/people/profileextractors.py | 5 - geonode/people/tests.py | 2 +- geonode/proxy/tests.py | 19 +- geonode/proxy/views.py | 3 +- geonode/resource/api/views.py | 53 +- geonode/resource/manager.py | 159 +-- geonode/resource/tests.py | 67 +- geonode/security/tests.py | 45 +- geonode/security/views.py | 29 - geonode/services/tests.py | 4 +- geonode/services/utils.py | 39 - geonode/settings.py | 71 +- geonode/social/signals.py | 30 +- geonode/static/geonode/js/upload/LayerInfo.js | 6 +- geonode/tests/bdd/e2e/test_login.py | 90 -- geonode/tests/smoke.py | 9 - geonode/tests/test_utils.py | 51 +- geonode/tests/utils.py | 69 +- geonode/thumbs/tests/test_integration.py | 4 +- geonode/thumbs/utils.py | 5 - geonode/upload/__init__.py | 12 +- geonode/upload/admin.py | 2 - geonode/upload/api/__init__.py | 2 +- geonode/upload/api/exceptions.py | 51 +- geonode/upload/api/permissions.py | 3 +- geonode/upload/api/serializer.py | 85 ++ geonode/upload/api/serializers.py | 254 ----- geonode/upload/api/tests.py | 855 ++++------------ geonode/upload/api/tests_old.py | 635 ++++++++++++ geonode/upload/api/urls.py | 7 - geonode/upload/api/views.py | 342 ++++++- geonode/upload/apps.py | 32 +- geonode/upload/celery_app.py | 29 + geonode/upload/celery_tasks.py | 773 ++++++++++++++ geonode/upload/datastore.py | 71 ++ geonode/upload/db_router.py | 58 ++ geonode/upload/files.py | 296 +----- geonode/upload/forms.py | 95 -- geonode/upload/handlers/README.md | 292 ++++++ .../{templatetags => handlers}/__init__.py | 2 +- geonode/upload/handlers/apps.py | 61 ++ geonode/upload/handlers/base.py | 348 +++++++ geonode/upload/handlers/common/__init__.py | 18 + geonode/upload/handlers/common/metadata.py | 131 +++ geonode/upload/handlers/common/raster.py | 570 +++++++++++ geonode/upload/handlers/common/remote.py | 292 ++++++ geonode/upload/handlers/common/serializer.py | 39 + geonode/upload/handlers/common/test_remote.py | 153 +++ .../upload/handlers/common/tests_raster.py | 95 ++ .../upload/handlers/common/tests_vector.py | 368 +++++++ geonode/upload/handlers/common/vector.py | 941 ++++++++++++++++++ geonode/upload/handlers/csv/__init__.py | 18 + geonode/upload/handlers/csv/exceptions.py | 27 + geonode/upload/handlers/csv/handler.py | 233 +++++ geonode/upload/handlers/csv/tests.py | 182 ++++ geonode/upload/handlers/geojson/__init__.py | 18 + geonode/upload/handlers/geojson/exceptions.py | 27 + geonode/upload/handlers/geojson/handler.py | 139 +++ geonode/upload/handlers/geojson/tests.py | 151 +++ geonode/upload/handlers/geotiff/__init__.py | 18 + geonode/upload/handlers/geotiff/exceptions.py | 27 + geonode/upload/handlers/geotiff/handler.py | 100 ++ geonode/upload/handlers/geotiff/tests.py | 108 ++ geonode/upload/handlers/gpkg/__init__.py | 18 + geonode/upload/handlers/gpkg/exceptions.py | 27 + geonode/upload/handlers/gpkg/handler.py | 158 +++ geonode/upload/handlers/gpkg/tasks.py | 39 + geonode/upload/handlers/gpkg/tests.py | 169 ++++ geonode/upload/handlers/kml/__init__.py | 18 + geonode/upload/handlers/kml/exceptions.py | 27 + geonode/upload/handlers/kml/handler.py | 153 +++ geonode/upload/handlers/kml/tests.py | 115 +++ geonode/upload/handlers/remote/__init__.py | 0 .../handlers/remote/serializers/__init__.py | 18 + .../upload/handlers/remote/serializers/wms.py | 35 + .../upload/handlers/remote/tests/__init__.py | 18 + .../handlers/remote/tests/test_3dtiles.py | 175 ++++ .../upload/handlers/remote/tests/test_wms.py | 251 +++++ geonode/upload/handlers/remote/tiles3d.py | 109 ++ geonode/upload/handlers/remote/wms.py | 163 +++ geonode/upload/handlers/shapefile/__init__.py | 18 + .../upload/handlers/shapefile/exceptions.py | 27 + geonode/upload/handlers/shapefile/handler.py | 212 ++++ .../upload/handlers/shapefile/serializer.py | 65 ++ geonode/upload/handlers/shapefile/tests.py | 178 ++++ geonode/upload/handlers/sld/__init__.py | 18 + geonode/upload/handlers/sld/exceptions.py | 27 + geonode/upload/handlers/sld/handler.py | 94 ++ geonode/upload/handlers/sld/tests.py | 107 ++ geonode/upload/handlers/tests.py | 83 ++ geonode/upload/handlers/tiles3d/__init__.py | 18 + geonode/upload/handlers/tiles3d/exceptions.py | 27 + geonode/upload/handlers/tiles3d/handler.py | 319 ++++++ geonode/upload/handlers/tiles3d/tests.py | 468 +++++++++ geonode/upload/handlers/tiles3d/utils.py | 245 +++++ geonode/upload/handlers/utils.py | 159 +++ geonode/upload/handlers/xml/__init__.py | 18 + geonode/upload/handlers/xml/exceptions.py | 27 + geonode/upload/handlers/xml/handler.py | 93 ++ geonode/upload/handlers/xml/serializer.py | 35 + geonode/upload/handlers/xml/tests.py | 105 ++ .../migrations/0040_importer_introduction.py | 39 + .../0042_resourcehandlerinfo_kwargs.py | 20 + .../0043_resourcehandlerinfo_execution_id.py | 24 + ...d_resourcehandlerinfo_execution_request.py | 17 + .../0045_fixup_dynamic_shema_table_names.py | 37 + .../migrations/0046_dataset_migration.py | 45 + ...7_alter_resourcehandlerinfo_id_and_more.py | 28 + .../0048_alter_resourcehandlerinfo_id.py | 18 + .../0049_move_data_from_importer_to_upload.py | 52 + geonode/upload/models.py | 235 +---- geonode/upload/orchestrator.py | 341 +++++++ geonode/upload/publisher.py | 214 ++++ geonode/upload/settings.py | 41 + geonode/upload/tasks.py | 5 +- geonode/upload/templates/__init__.py | 18 + .../templates/upload/dataset_upload_base.html | 22 - .../templates/upload/dataset_upload_crs.html | 233 ----- .../templates/upload/dataset_upload_csv.html | 90 -- .../upload/dataset_upload_error.html | 7 - .../upload/dataset_upload_invalid.html | 9 - .../upload/dataset_upload_metadata_base.html | 2 +- .../templates/upload/dataset_upload_time.html | 442 -------- .../upload/templates/upload/no_upload.html | 20 - geonode/upload/templatetags/upload_tags.py | 93 -- geonode/upload/tests/__init__.py | 165 +-- geonode/upload/tests/end2end/__init__.py | 18 + geonode/upload/tests/end2end/integration.py | 733 ++++++++++++++ geonode/upload/tests/end2end/test_end2end.py | 497 +++++++++ .../upload/tests/end2end/test_end2end_copy.py | 216 ++++ .../tests/fixture/3dtilesample/README.md | 2 + .../tests/fixture/3dtilesample/invalid.zip | 0 .../fixture/3dtilesample/invalid_tileset.json | 3 + .../tests/fixture/3dtilesample/tileset.json | 16 + .../3dtilesample/tileset_with_region.json | 21 + .../fixture/3dtilesample/valid_3dtiles.zip | Bin 0 -> 1333 bytes geonode/upload/tests/fixture/invalid.csv | 0 geonode/upload/tests/fixture/invalid.geojson | 11 + geonode/upload/tests/fixture/invalid.gpkg | Bin 0 -> 122880 bytes geonode/upload/tests/fixture/missing_geom.csv | 2 + geonode/upload/tests/fixture/missing_lat.csv | 2 + geonode/upload/tests/fixture/missing_long.csv | 2 + geonode/upload/tests/fixture/noCrsTable.gpkg | Bin 0 -> 106496 bytes geonode/upload/tests/fixture/test_raster.tif | Bin 0 -> 652 bytes geonode/upload/tests/fixture/valid.csv | 4 + geonode/upload/tests/fixture/valid.geojson | 18 + geonode/upload/tests/fixture/valid.gpkg | Bin 0 -> 122880 bytes geonode/upload/tests/fixture/valid.kml | 37 + geonode/upload/tests/fixture/valid.zip | Bin 0 -> 13924 bytes geonode/upload/tests/integration.py | 20 +- geonode/upload/tests/test_files.py | 43 - geonode/upload/tests/unit/__init__.py | 0 geonode/upload/tests/unit/test_dastore.py | 67 ++ geonode/upload/tests/unit/test_models.py | 57 ++ .../upload/tests/unit/test_orchestrator.py | 373 +++++++ geonode/upload/tests/unit/test_publisher.py | 104 ++ geonode/upload/tests/unit/test_task.py | 667 +++++++++++++ geonode/upload/tests/{ => unit}/test_utils.py | 36 - geonode/upload/tests/utils.py | 98 +- geonode/upload/upload.py | 116 --- geonode/upload/upload_validators.py | 237 ----- geonode/upload/uploadhandler.py | 18 + geonode/upload/urls.py | 11 +- geonode/upload/utils.py | 640 ++---------- geonode/upload/views.py | 39 +- geonode/urls.py | 15 +- geonode/utils.py | 258 +---- geonode/views.py | 13 - package/debian/changelog | 2 +- requirements.txt | 9 +- setup.cfg | 1 - test.sh | 2 +- 207 files changed, 14553 insertions(+), 6038 deletions(-) delete mode 100644 geonode/favorite/utils.py delete mode 100644 geonode/geoserver/tests/test_manager.py delete mode 100644 geonode/geoserver/upload.py delete mode 100644 geonode/tests/bdd/e2e/test_login.py create mode 100644 geonode/upload/api/serializer.py delete mode 100644 geonode/upload/api/serializers.py create mode 100644 geonode/upload/api/tests_old.py create mode 100644 geonode/upload/celery_app.py create mode 100644 geonode/upload/celery_tasks.py create mode 100644 geonode/upload/datastore.py create mode 100644 geonode/upload/db_router.py delete mode 100644 geonode/upload/forms.py create mode 100644 geonode/upload/handlers/README.md rename geonode/upload/{templatetags => handlers}/__init__.py (96%) create mode 100644 geonode/upload/handlers/apps.py create mode 100644 geonode/upload/handlers/base.py create mode 100644 geonode/upload/handlers/common/__init__.py create mode 100644 geonode/upload/handlers/common/metadata.py create mode 100644 geonode/upload/handlers/common/raster.py create mode 100755 geonode/upload/handlers/common/remote.py create mode 100644 geonode/upload/handlers/common/serializer.py create mode 100644 geonode/upload/handlers/common/test_remote.py create mode 100644 geonode/upload/handlers/common/tests_raster.py create mode 100644 geonode/upload/handlers/common/tests_vector.py create mode 100644 geonode/upload/handlers/common/vector.py create mode 100644 geonode/upload/handlers/csv/__init__.py create mode 100644 geonode/upload/handlers/csv/exceptions.py create mode 100644 geonode/upload/handlers/csv/handler.py create mode 100644 geonode/upload/handlers/csv/tests.py create mode 100644 geonode/upload/handlers/geojson/__init__.py create mode 100644 geonode/upload/handlers/geojson/exceptions.py create mode 100644 geonode/upload/handlers/geojson/handler.py create mode 100644 geonode/upload/handlers/geojson/tests.py create mode 100644 geonode/upload/handlers/geotiff/__init__.py create mode 100644 geonode/upload/handlers/geotiff/exceptions.py create mode 100644 geonode/upload/handlers/geotiff/handler.py create mode 100644 geonode/upload/handlers/geotiff/tests.py create mode 100644 geonode/upload/handlers/gpkg/__init__.py create mode 100644 geonode/upload/handlers/gpkg/exceptions.py create mode 100644 geonode/upload/handlers/gpkg/handler.py create mode 100644 geonode/upload/handlers/gpkg/tasks.py create mode 100644 geonode/upload/handlers/gpkg/tests.py create mode 100644 geonode/upload/handlers/kml/__init__.py create mode 100644 geonode/upload/handlers/kml/exceptions.py create mode 100644 geonode/upload/handlers/kml/handler.py create mode 100644 geonode/upload/handlers/kml/tests.py create mode 100644 geonode/upload/handlers/remote/__init__.py create mode 100644 geonode/upload/handlers/remote/serializers/__init__.py create mode 100644 geonode/upload/handlers/remote/serializers/wms.py create mode 100755 geonode/upload/handlers/remote/tests/__init__.py create mode 100644 geonode/upload/handlers/remote/tests/test_3dtiles.py create mode 100644 geonode/upload/handlers/remote/tests/test_wms.py create mode 100644 geonode/upload/handlers/remote/tiles3d.py create mode 100644 geonode/upload/handlers/remote/wms.py create mode 100644 geonode/upload/handlers/shapefile/__init__.py create mode 100644 geonode/upload/handlers/shapefile/exceptions.py create mode 100644 geonode/upload/handlers/shapefile/handler.py create mode 100644 geonode/upload/handlers/shapefile/serializer.py create mode 100644 geonode/upload/handlers/shapefile/tests.py create mode 100644 geonode/upload/handlers/sld/__init__.py create mode 100644 geonode/upload/handlers/sld/exceptions.py create mode 100644 geonode/upload/handlers/sld/handler.py create mode 100644 geonode/upload/handlers/sld/tests.py create mode 100644 geonode/upload/handlers/tests.py create mode 100755 geonode/upload/handlers/tiles3d/__init__.py create mode 100755 geonode/upload/handlers/tiles3d/exceptions.py create mode 100755 geonode/upload/handlers/tiles3d/handler.py create mode 100755 geonode/upload/handlers/tiles3d/tests.py create mode 100644 geonode/upload/handlers/tiles3d/utils.py create mode 100644 geonode/upload/handlers/utils.py create mode 100644 geonode/upload/handlers/xml/__init__.py create mode 100644 geonode/upload/handlers/xml/exceptions.py create mode 100644 geonode/upload/handlers/xml/handler.py create mode 100644 geonode/upload/handlers/xml/serializer.py create mode 100644 geonode/upload/handlers/xml/tests.py create mode 100644 geonode/upload/migrations/0040_importer_introduction.py create mode 100644 geonode/upload/migrations/0042_resourcehandlerinfo_kwargs.py create mode 100644 geonode/upload/migrations/0043_resourcehandlerinfo_execution_id.py create mode 100644 geonode/upload/migrations/0044_rename_execution_id_resourcehandlerinfo_execution_request.py create mode 100644 geonode/upload/migrations/0045_fixup_dynamic_shema_table_names.py create mode 100644 geonode/upload/migrations/0046_dataset_migration.py create mode 100644 geonode/upload/migrations/0047_alter_resourcehandlerinfo_id_and_more.py create mode 100644 geonode/upload/migrations/0048_alter_resourcehandlerinfo_id.py create mode 100644 geonode/upload/migrations/0049_move_data_from_importer_to_upload.py create mode 100644 geonode/upload/orchestrator.py create mode 100644 geonode/upload/publisher.py create mode 100644 geonode/upload/settings.py create mode 100644 geonode/upload/templates/__init__.py delete mode 100644 geonode/upload/templates/upload/dataset_upload_base.html delete mode 100644 geonode/upload/templates/upload/dataset_upload_crs.html delete mode 100644 geonode/upload/templates/upload/dataset_upload_csv.html delete mode 100644 geonode/upload/templates/upload/dataset_upload_error.html delete mode 100644 geonode/upload/templates/upload/dataset_upload_invalid.html delete mode 100644 geonode/upload/templates/upload/dataset_upload_time.html delete mode 100644 geonode/upload/templates/upload/no_upload.html delete mode 100644 geonode/upload/templatetags/upload_tags.py create mode 100644 geonode/upload/tests/end2end/__init__.py create mode 100644 geonode/upload/tests/end2end/integration.py create mode 100644 geonode/upload/tests/end2end/test_end2end.py create mode 100644 geonode/upload/tests/end2end/test_end2end_copy.py create mode 100755 geonode/upload/tests/fixture/3dtilesample/README.md create mode 100755 geonode/upload/tests/fixture/3dtilesample/invalid.zip create mode 100755 geonode/upload/tests/fixture/3dtilesample/invalid_tileset.json create mode 100755 geonode/upload/tests/fixture/3dtilesample/tileset.json create mode 100755 geonode/upload/tests/fixture/3dtilesample/tileset_with_region.json create mode 100755 geonode/upload/tests/fixture/3dtilesample/valid_3dtiles.zip create mode 100644 geonode/upload/tests/fixture/invalid.csv create mode 100644 geonode/upload/tests/fixture/invalid.geojson create mode 100644 geonode/upload/tests/fixture/invalid.gpkg create mode 100644 geonode/upload/tests/fixture/missing_geom.csv create mode 100644 geonode/upload/tests/fixture/missing_lat.csv create mode 100644 geonode/upload/tests/fixture/missing_long.csv create mode 100755 geonode/upload/tests/fixture/noCrsTable.gpkg create mode 100644 geonode/upload/tests/fixture/test_raster.tif create mode 100644 geonode/upload/tests/fixture/valid.csv create mode 100644 geonode/upload/tests/fixture/valid.geojson create mode 100755 geonode/upload/tests/fixture/valid.gpkg create mode 100644 geonode/upload/tests/fixture/valid.kml create mode 100644 geonode/upload/tests/fixture/valid.zip delete mode 100644 geonode/upload/tests/test_files.py create mode 100644 geonode/upload/tests/unit/__init__.py create mode 100644 geonode/upload/tests/unit/test_dastore.py create mode 100644 geonode/upload/tests/unit/test_models.py create mode 100644 geonode/upload/tests/unit/test_orchestrator.py create mode 100644 geonode/upload/tests/unit/test_publisher.py create mode 100644 geonode/upload/tests/unit/test_task.py rename geonode/upload/tests/{ => unit}/test_utils.py (63%) delete mode 100644 geonode/upload/upload.py delete mode 100644 geonode/upload/upload_validators.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 2945c6ce190..7f485f1c73a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -110,7 +110,7 @@ workflows: codecov_name: main_tests load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''security'\'' not in a and '\''geoserver'\'' not in a]))") geonode.thumbs.tests geonode.people.tests geonode.people.socialaccount.providers.geonode_openid_connect.tests + test_suite: ./test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''security'\'' not in a and '\''geoserver'\'' not in a and '\''upload'\'' not in a]))") geonode.thumbs.tests geonode.people.tests geonode.people.socialaccount.providers.geonode_openid_connect.tests - build: name: geonode_test_security codecov_name: security_tests @@ -128,14 +128,19 @@ workflows: codecov_name: api load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh geonode.api.tests geonode.base.api.tests geonode.layers.api.tests geonode.maps.api.tests geonode.documents.api.tests geonode.geoapps.api.tests geonode.upload.api.tests + test_suite: ./test.sh geonode.api.tests geonode.base.api.tests geonode.layers.api.tests geonode.maps.api.tests geonode.documents.api.tests geonode.geoapps.api.tests - build: name: geonode_test_csw codecov_name: csw load_docker_cache: false save_docker_cache: false test_suite: ./test.sh geonode.tests.csw geonode.catalogue.backends.tests - + - build: + name: geonode_upload + codecov_name: importer + load_docker_cache: false + save_docker_cache: false + test_suite: ./test.sh geonode.upload # TODO # - build: # name: geonode_test_integration_upload diff --git a/.env_test b/.env_test index 8026c6c0002..1695e29a786 100644 --- a/.env_test +++ b/.env_test @@ -39,7 +39,7 @@ GEONODE_DB_CONN_TOUT=5 DEFAULT_BACKEND_DATASTORE=datastore BROKER_URL=amqp://guest:guest@rabbitmq:5672/ CELERY_BEAT_SCHEDULER=celery.beat:PersistentScheduler -ASYNC_SIGNALS=True +ASYNC_SIGNALS=False SITEURL=http://localhost:8000/ @@ -64,7 +64,7 @@ NGINX_BASE_URL=http://localhost # port where the server can be reached on HTTPS HTTP_HOST=localhost HTTPS_HOST= - +POSTGRESQL_MAX_CONNECTIONS=100 HTTP_PORT=8000 HTTPS_PORT=443 @@ -237,4 +237,4 @@ DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=100 MICROSOFT_TENANT_ID= AZURE_CLIENT_ID= AZURE_SECRET_KEY= -AZURE_KEY= +AZURE_KEY= \ No newline at end of file diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 1a162f8cf4f..18e1d8b53ac 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -80,15 +80,15 @@ services: retries: 5 start_period: 30s - # Gets and installs letsencrypt certificates - letsencrypt: - image: geonode/letsencrypt:2.6.0-latest - container_name: letsencrypt4${COMPOSE_PROJECT_NAME} - env_file: - - .env_test - volumes: - - nginx-certificates:/geonode-certificates - restart: unless-stopped + # # Gets and installs letsencrypt certificates + # letsencrypt: + # image: geonode/letsencrypt:2.6.0-latest + # container_name: letsencrypt4${COMPOSE_PROJECT_NAME} + # env_file: + # - .env_test + # volumes: + # - nginx-certificates:/geonode-certificates + # restart: unless-stopped # Geoserver backend geoserver: diff --git a/geonode/__init__.py b/geonode/__init__.py index 4c56c53ace5..4f90e75c17b 100644 --- a/geonode/__init__.py +++ b/geonode/__init__.py @@ -28,7 +28,7 @@ def get_version(): return geonode.version.get_version(__version__) -def main(global_settings, **settings): +def main(_, **settings): from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings.get("django_settings")) diff --git a/geonode/assets/tests.py b/geonode/assets/tests.py index 0401a7a0943..6725db5cf80 100644 --- a/geonode/assets/tests.py +++ b/geonode/assets/tests.py @@ -226,7 +226,26 @@ def test_download_file(self): u, _ = get_user_model().objects.get_or_create(username="admin") self.assertTrue(self.client.login(username="admin", password="admin"), "Login failed") - asset = self._setup_test(u) + asset_handler = asset_handler_registry.get_default_handler() + asset = asset_handler.create( + title="Test Asset", + description="Description of test asset", + type="NeverMind", + owner=u, + files=[ONE_JSON], + clone_files=True, + ) + asset.save() + self.assertIsInstance(asset, LocalAsset) + + reloaded = LocalAsset.objects.get(pk=asset.pk) + + # put two more files in the asset dir + asset_dir = os.path.dirname(reloaded.location[0]) + sub_dir = os.path.join(asset_dir, "subdir") + os.mkdir(sub_dir) + shutil.copy(TWO_JSON, asset_dir) + shutil.copy(THREE_JSON, sub_dir) for path, key in ((None, "one"), ("one.json", "one"), ("two.json", "two"), ("subdir/three.json", "three")): # args = [asset.pk, path] if path else [asset.pk] diff --git a/geonode/base/__init__.py b/geonode/base/__init__.py index dba87a5d75e..f799838c06d 100644 --- a/geonode/base/__init__.py +++ b/geonode/base/__init__.py @@ -72,9 +72,3 @@ def register_event(request, event_type, resource): raise ValueError(f"Invalid resource: {resource}") if request and hasattr(request, "register_event"): request.register_event(event_type, resource_type, resource_name, resource_id) - - -def register_proxy_event(request): - """ - Process request to geoserver proxy. Extract layer and ows type - """ diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index 2c839503d74..e318fd06e61 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2408,17 +2408,9 @@ def test_resource_service_copy_with_perms_doc(self): @override_settings(CELERY_TASK_ALWAYS_EAGER=True) def test_resource_service_copy_with_perms_map(self): - files = os.path.join(gisdata.GOOD_DATA, "vector/single_point.shp") - files_as_dict, _ = get_files(files) - resource = Document.objects.create( - owner=get_user_model().objects.get(username="admin"), - alternate="geonode:test_copy", - resource_type="map", - uuid=str(uuid4()), - ) - _, _ = create_asset_and_link( - resource, get_user_model().objects.get(username="admin"), list(files_as_dict.values()) - ) + + resource = create_single_map(name="test_copy") + self._assertCloningWithPerms(resource) def _assertCloningWithPerms(self, resource): @@ -2430,7 +2422,7 @@ def _assertCloningWithPerms(self, resource): resource.set_permissions(_perms) copy_url = reverse("importer_resource_copy", kwargs={"pk": resource.pk}) response = self.client.put(copy_url, data={"title": "cloned_resource"}) - self.assertIn(response.status_code, [403, 404]) + self.assertIn(response.status_code, [302, 403, 404]) # set perms to enable user clone resource # bobby can copy the resource since he has all the perms needed _perms = { diff --git a/geonode/base/auth.py b/geonode/base/auth.py index e56d03edb01..d57c1efe264 100644 --- a/geonode/base/auth.py +++ b/geonode/base/auth.py @@ -47,23 +47,6 @@ def extract_user_from_headers(request): return user -def extract_headers(request): - """ - Extracts headers from the Django request object - :param request: The current django.http.HttpRequest object - :return: a dictionary with OAuthLib needed headers - """ - headers = request.META.copy() - if "wsgi.input" in headers: - del headers["wsgi.input"] - if "wsgi.errors" in headers: - del headers["wsgi.errors"] - if "HTTP_AUTHORIZATION" in headers: - headers["Authorization"] = headers["HTTP_AUTHORIZATION"] - - return headers - - def make_token_expiration(seconds=86400): _expire_seconds = getattr(settings, "ACCESS_TOKEN_EXPIRE_SECONDS", seconds) _expire_time = datetime.datetime.now(timezone.get_current_timezone()) diff --git a/geonode/base/bbox_utils.py b/geonode/base/bbox_utils.py index b589e1b9f16..b61b58e699a 100644 --- a/geonode/base/bbox_utils.py +++ b/geonode/base/bbox_utils.py @@ -62,13 +62,6 @@ def as_polygon(self): return DjangoPolygon.from_bbox((self.xmin, self.ymin, self.xmax, self.ymax)) -def normalize_x_value(value): - """ - Normalise x-axis value/longtitude to fall within [-180, 180] - """ - return ((value + 180) % 360) - 180 - - def polygon_from_bbox(bbox, srid=4326): """ Constructs a Polygon object with srid from a provided bbox. diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 6e01854d2ce..7f83bb4d418 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -29,7 +29,6 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from django.core import validators from django.db.models import Prefetch, Q from django.forms import models from django.forms.fields import ChoiceField, MultipleChoiceField @@ -659,22 +658,6 @@ class Meta: ) -class ValuesListField(forms.Field): - def to_python(self, value): - if value in validators.EMPTY_VALUES: - return [] - - value = [item.strip() for item in value.split(",") if item.strip()] - - return value - - def clean(self, value): - value = self.to_python(value) - self.validate(value) - self.run_validators(value) - return value - - class BatchEditForm(forms.Form): LANGUAGES = (("", "--------"),) + ALL_LANGUAGES group = forms.ModelChoiceField(label=_("Group"), queryset=Group.objects.all(), required=False) diff --git a/geonode/base/models.py b/geonode/base/models.py index 00297dca75b..80f8fa2b532 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -617,15 +617,6 @@ def cleanup_uploaded_files(resource_id): filename = f"{_resource.get_real_instance().resource_type}-{_resource.get_real_instance().uuid}" remove_thumbs(filename) - # Remove the uploaded sessions, if any - if "geonode.upload" in settings.INSTALLED_APPS: - from geonode.upload.models import Upload - - # Need to call delete one by one in order to invoke the - # 'delete' overridden method - for upload in Upload.objects.filter(resource_id=_resource.get_real_instance().id): - upload.delete() - class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase): """ diff --git a/geonode/catalogue/views.py b/geonode/catalogue/views.py index 8e64559f8c1..9e1a4efc853 100644 --- a/geonode/catalogue/views.py +++ b/geonode/catalogue/views.py @@ -201,16 +201,6 @@ def fst(value): return result -# from a resource object, build the corresponding metadata dict -# the aim is to handle the output format (csv, html or pdf) the same structure -def build_md_dict(resource): - md_dict = { - "r_uuid": {"label": "uuid", "value": resource.uuid}, - "r_title": {"label": "titre", "value": resource.title}, - } - return md_dict - - def get_keywords(resource): content = " " cursor = connection.cursor() diff --git a/geonode/client/conf.py b/geonode/client/conf.py index 5646907dd15..3d9982890db 100644 --- a/geonode/client/conf.py +++ b/geonode/client/conf.py @@ -38,14 +38,6 @@ def load_path_attr(path): return attr -def is_installed(package): - try: - __import__(package) - return True - except ImportError: - return False - - class GeoNodeClientAppConf(AppConf): LAYER_PREVIEW_LIBRARY = "geonode" HOOKSET = "geonode.client.hooksets.BaseHookSet" diff --git a/geonode/decorators.py b/geonode/decorators.py index 6f356167f81..e788f30632c 100644 --- a/geonode/decorators.py +++ b/geonode/decorators.py @@ -153,29 +153,6 @@ def view_or_apiauth(view, request, test_func, *args, **kwargs): return response -def has_perm_or_basicauth(perm, realm=""): - """ - This is similar to the above decorator 'logged_in_or_basicauth' - except that it requires the logged in user to have a specific - permission. - - Use: - - @logged_in_or_basicauth('asforums.view_forumcollection') - def your_view: - ... - - """ - - def view_decorator(func): - def wrapper(request, *args, **kwargs): - return view_or_basicauth(func, request, lambda u: u.has_perm(perm), realm, *args, **kwargs) - - return wrapper - - return view_decorator - - def superuser_only(function): """ Limit view to superusers only. @@ -289,16 +266,6 @@ def wrapper(request, *args, **kwargs): return view_decorator -def logged_in_or_apiauth(): - def view_decorator(func): - def wrapper(request, *args, **kwargs): - return view_or_apiauth(func, request, lambda u: u.is_authenticated, *args, **kwargs) - - return wrapper - - return view_decorator - - def superuser_or_apiauth(): def view_decorator(func): def wrapper(request, *args, **kwargs): @@ -307,11 +274,3 @@ def wrapper(request, *args, **kwargs): return wrapper return view_decorator - - -def dump_func_name(func): - def echo_func(*func_args, **func_kwargs): - logger.debug(f"Start func: {func.__name__}") - return func(*func_args, **func_kwargs) - - return echo_func diff --git a/geonode/documents/forms.py b/geonode/documents/forms.py index 29ab23d8a97..be83888c3a5 100644 --- a/geonode/documents/forms.py +++ b/geonode/documents/forms.py @@ -110,12 +110,6 @@ class Meta(ResourceBaseForm.Meta): ) -class DocumentDescriptionForm(forms.Form): - title = forms.CharField(max_length=300) - abstract = forms.CharField(max_length=2000, widget=forms.Textarea, required=False) - keywords = forms.CharField(max_length=500, required=False) - - class DocumentCreateForm(TranslationModelForm): """ The document upload form. diff --git a/geonode/favorite/utils.py b/geonode/favorite/utils.py deleted file mode 100644 index 985b1ed0d7c..00000000000 --- a/geonode/favorite/utils.py +++ /dev/null @@ -1,44 +0,0 @@ -######################################################################### -# -# 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.urls import reverse -from . import models - - -def get_favorite_info(user, content_object): - """ - return favorite info dict containing: - a. an add favorite url for the input parameters. - b. whether there is an existing Favorite for the input parameters. - c. a delete url (if there is an existing Favorite). - """ - result = {} - - url_content_type = type(content_object).__name__.lower() - result["add_url"] = reverse(f"add_favorite_{url_content_type}", args=[content_object.pk]) - - existing_favorite = models.Favorite.objects.favorite_for_user_and_content_object(user, content_object) - - if existing_favorite: - result["has_favorite"] = "true" - result["delete_url"] = reverse("delete_favorite", args=[existing_favorite.pk]) - else: - result["has_favorite"] = "false" - - return result diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index f3851567223..a152e4ab1bb 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -24,14 +24,10 @@ import uuid import json import errno -import typing import logging import datetime -import tempfile import traceback -import dataclasses -from shutil import copyfile from itertools import cycle from collections import defaultdict from os.path import basename, splitext, isfile @@ -1321,16 +1317,6 @@ def get_wcs_record(instance, retry=True): raise GeoNodeException(msg) -def get_coverage_grid_extent(instance): - """ - Returns a list of integers with the size of the coverage - extent in pixels - """ - instance_wcs = get_wcs_record(instance) - grid = instance_wcs.grid - return [(int(h) - int(l) + 1) for h, l in zip(grid.highlimits, grid.lowlimits)] - - GEOSERVER_LAYER_TYPES = { "vector": FeatureType.resource_type, "raster": Coverage.resource_type, @@ -1645,7 +1631,7 @@ def _stylefilterparams_geowebcache_dataset(dataset_name): # check/write GWC filter parameters body = None - tree = dlxml.fromstring(_) + tree = dlxml.fromstring(content.encode()) param_filters = tree.findall("parameterFilters") if param_filters and len(param_filters) > 0: if not param_filters[0].findall("styleParameterFilter"): @@ -1822,40 +1808,6 @@ def set_time_info(layer, attribute, end_attribute, presentation, precision_value gs_catalog.save(resource) -def get_time_info(layer): - """Get the configured time dimension metadata for the layer as a dict. - - The keys of the dict will be those of the parameters of `set_time_info`. - - :returns: dict of values or None if not configured - """ - layer = gs_catalog.get_layer(layer.name) - if layer is None: - raise ValueError(f"no such layer: {layer.name}") - resource = layer.resource if layer else None - if not resource: - resources = gs_catalog.get_resources(stores=[layer.name]) - if resources: - resource = resources[0] - - info = resource.metadata.get("time", None) if resource.metadata else None - vals = None - if info: - value = step = None - resolution = info.resolution_str() - if resolution: - value, step = resolution.split() - vals = dict( - enabled=info.enabled, - attribute=info.attribute, - end_attribute=info.end_attribute, - presentation=info.presentation, - precision_value=value, - precision_step=step, - ) - return vals - - ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"] _wms = None @@ -1901,85 +1853,6 @@ def _create_geofence_client(): } -def _dump_image_spec(request_body, image_spec): - millis = int(round(time.time() * 1000)) - try: - with tempfile.TemporaryDirectory() as tmp_dir: - _request_body_file_name = os.path.join(tmp_dir, f"request_body_{millis}.dump") - _image_spec_file_name = os.path.join(tmp_dir, f"image_spec_{millis}.dump") - with open(_request_body_file_name, "w") as _request_body_file: - _request_body_file.write(f"{request_body}") - copyfile(_request_body_file_name, os.path.join(tempfile.gettempdir(), f"request_body_{millis}.dump")) - with open(_image_spec_file_name, "w") as _image_spec_file: - _image_spec_file.write(f"{image_spec}") - copyfile(_image_spec_file_name, os.path.join(tempfile.gettempdir(), f"image_spec_{millis}.dump")) - return f"Dumping image_spec to: {os.path.join(tempfile.gettempdir(), f'image_spec_{millis}.dump')}" - except Exception as e: - logger.exception(e) - return f"Unable to dump image_spec for request: {request_body}" - - -def mosaic_delete_first_granule(cat, layer): - # - since GeoNode will uploade the first granule again through the Importer, we need to / - # delete the one created by the gs_config - cat._cache.clear() - store = cat.get_store(layer) - coverages = cat.mosaic_coverages(store) - - granule_id = f"{layer}.1" - - cat.mosaic_delete_granule(coverages["coverages"]["coverage"][0]["name"], store, granule_id) - - -def set_time_dimension( - cat, - name, - workspace, - time_presentation, - time_presentation_res, - time_presentation_default_value, - time_presentation_reference_value, -): - # configure the layer time dimension as LIST - presentation = time_presentation - if not presentation: - presentation = "LIST" - - resolution = None - if time_presentation == "DISCRETE_INTERVAL": - resolution = time_presentation_res - - strategy = None - if time_presentation_default_value and not time_presentation_default_value == "": - strategy = time_presentation_default_value - - timeInfo = DimensionInfo( - "time", - "true", - presentation, - resolution, - "ISO8601", - None, - attribute="time", - strategy=strategy, - reference_value=time_presentation_reference_value, - ) - - layer = cat.get_layer(name) - resource = layer.resource if layer else None - if not resource: - resources = cat.get_resources(stores=[name]) or cat.get_resources(stores=[name], workspaces=[workspace]) - if resources: - resource = resources[0] - - if not resource: - logger.exception(f"No resource could be found on GeoServer with name {name}") - raise Exception(f"No resource could be found on GeoServer with name {name}") - - resource.metadata = {"time": timeInfo} - cat.save(resource) - - # main entry point to create a thumbnail - will use implementation # defined in settings.THUMBNAIL_GENERATOR (see settings.py) def create_gs_thumbnail(instance, overwrite=False, check_bbox=False): @@ -2029,13 +1902,8 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): if not _is_remote_instance: values = {"title": instance.title, "abstract": instance.raw_abstract} _tries = 0 - _max_tries = getattr(ogc_server_settings, "MAX_RETRIES", 3) values, gs_resource = fetch_gs_resource(instance, values, _tries) - while not gs_resource and _tries < _max_tries: - values, gs_resource = fetch_gs_resource(instance, values, _tries) - _tries += 1 - time.sleep(3) if gs_resource: logger.debug(f"Found geoserver resource for this dataset: {instance.name}") @@ -2207,36 +2075,6 @@ def select_relevant_files(allowed_extensions, files): return result -@dataclasses.dataclass() -class SpatialFilesLayerType: - base_file: str - scan_hint: str - spatial_files: typing.List - dataset_type: typing.Optional[str] = None - - -def get_spatial_files_dataset_type(allowed_extensions, files, charset="UTF-8") -> SpatialFilesLayerType: - """Reutnrs 'vector' or 'raster' whether a file from the allowed extensins has been identified.""" - from geonode.upload.files import scan_file - - allowed_file = select_relevant_files(allowed_extensions, files) - if not allowed_file or len(allowed_file) != 1: - return None - base_file = allowed_file[0] - spatial_files = scan_file(base_file, charset=charset) - the_dataset_type = get_dataset_type(spatial_files) - if the_dataset_type not in (FeatureType.resource_type, Coverage.resource_type): - return None - spatial_files_type = SpatialFilesLayerType( - base_file=base_file, - scan_hint=None, - spatial_files=spatial_files, - dataset_type="vector" if the_dataset_type == FeatureType.resource_type else "raster", - ) - - return spatial_files_type - - def get_dataset_type(spatial_files): """Returns 'FeatureType.resource_type' or 'Coverage.resource_type' accordingly to the provided SpatialFiles""" if spatial_files.archive is not None: diff --git a/geonode/geoserver/manager.py b/geonode/geoserver/manager.py index e3cc4a69378..8dd6803d626 100644 --- a/geonode/geoserver/manager.py +++ b/geonode/geoserver/manager.py @@ -17,13 +17,10 @@ # ######################################################################### -import os import typing import logging import tempfile -import dataclasses -from gsimporter.api import Session from django.conf import settings from django.db.models.query import QuerySet @@ -31,9 +28,7 @@ from django.contrib.auth import get_user_model from geonode.maps.models import Map -from geonode.base import enumerations from geonode.layers.models import Dataset -from geonode.upload.models import Upload from geonode.base.models import ResourceBase from geonode.utils import get_dataset_workspace from geonode.services.enumerations import CASCADED @@ -49,17 +44,11 @@ from .geofence import AutoPriorityBatch from .tasks import geoserver_set_style, geoserver_delete_map, geoserver_create_style, geoserver_cascading_delete from .helpers import ( - SpatialFilesLayerType, gs_catalog, - gs_uploader, - set_styles, set_time_info, ogc_server_settings, - get_spatial_files_dataset_type, sync_instance_with_geoserver, - set_attributes_from_geoserver, create_gs_thumbnail, - create_geoserver_db_featurestore, geofence, gf_utils, ) @@ -74,16 +63,6 @@ logger = logging.getLogger(__name__) -@dataclasses.dataclass() -class GeoServerImporterSessionInfo: - upload_session: Upload - import_session: Session - spatial_files_type: SpatialFilesLayerType - dataset_name: typing.AnyStr - workspace: typing.AnyStr - target_store: typing.AnyStr - - class GeoServerResourceManager(ResourceManagerInterface): def search(self, filter: dict, /, resource_type: typing.Optional[object]) -> QuerySet: return resource_type.objects.none() @@ -155,263 +134,6 @@ def update( instance = _synced_resource or instance return instance - def ingest( - self, - files: typing.List[str], - /, - uuid: str = None, - resource_type: typing.Optional[object] = None, - defaults: dict = {}, - **kwargs, - ) -> ResourceBase: - instance = ResourceManager._get_instance(uuid) - if instance and isinstance(instance.get_real_instance(), Dataset): - instance = self.import_dataset( - "import_dataset", - instance.uuid, - instance=instance, - files=files, - user=defaults.get("user", instance.owner), - defaults=defaults, - action_type="create", - **kwargs, - ) - return instance - - def copy( - self, instance: ResourceBase, /, uuid: str = None, owner: settings.AUTH_USER_MODEL = None, defaults: dict = {} - ) -> ResourceBase: - if uuid and instance: - _resource = ResourceManager._get_instance(uuid) - if _resource and isinstance(_resource.get_real_instance(), Dataset): - importer_session_opts = defaults.get("importer_session_opts", {}) - if not importer_session_opts: - _src_upload_session = Upload.objects.filter(resource=instance.get_real_instance().resourcebase_ptr) - if _src_upload_session.exists(): - _src_upload_session = _src_upload_session.get() - if _src_upload_session and _src_upload_session.get_session: - try: - _src_importer_session = _src_upload_session.get_session.import_session.reload() - importer_session_opts.update({"transforms": _src_importer_session.tasks[0].transforms}) - except Exception as e: - logger.exception(e) - return self.import_dataset( - "import_dataset", - uuid, - instance=_resource, - files=defaults.get("files", None), - user=defaults.get("user", _resource.owner), - defaults=defaults, - action_type="create", - importer_session_opts=importer_session_opts, - ) - return _resource - - def append(self, instance: ResourceBase, vals: dict = {}, *args, **kwargs) -> ResourceBase: - if instance and isinstance(instance.get_real_instance(), Dataset): - return self.import_dataset( - "import_dataset", - instance.uuid, - instance=instance, - files=vals.get("files", None), - user=vals.get("user", instance.owner), - action_type="append", - importer_session_opts=vals.get("importer_session_opts", None), - **kwargs, - ) - return instance - - def replace(self, instance: ResourceBase, vals: dict = {}, *args, **kwargs) -> ResourceBase: - if instance and isinstance(instance.get_real_instance(), Dataset): - return self.import_dataset( - "import_dataset", - instance.uuid, - instance=instance, - files=vals.get("files", None), - user=vals.get("user", instance.owner), - action_type="replace", - importer_session_opts=vals.get("importer_session_opts", None), - **kwargs, - ) - return instance - - def import_dataset(self, method: str, uuid: str, /, instance: ResourceBase = None, **kwargs) -> ResourceBase: - instance = instance or ResourceManager._get_instance(uuid) - - if instance and isinstance(instance.get_real_instance(), Dataset): - try: - _gs_import_session_info = self._execute_resource_import( - instance, - kwargs.get("files", None), - kwargs.get("user", instance.owner), - action_type=kwargs.get("action_type", "create"), - importer_session_opts=kwargs.get("importer_session_opts", None), - ) - import_session = _gs_import_session_info.import_session - if import_session: - if import_session.state == enumerations.STATE_PENDING: - task = None - native_crs = None - target_crs = "EPSG:4326" - for _task in import_session.tasks: - # CRS missing/unknown - if _task.state == "NO_CRS": - task = _task - native_crs = _task.layer.srs - break - if not native_crs: - native_crs = "EPSG:4326" - if task: - task.set_srs(native_crs) - - transform = { - "type": "ReprojectTransform", - "source": native_crs, - "target": target_crs, - } - task.remove_transforms([transform], by_field="type", save=False) - task.add_transforms([transform], save=False) - task.save_transforms() - # Starting import process - import_session.commit() - import_session = import_session.reload() - _gs_import_session_info.import_session = import_session - _gs_import_session_info.dataset_name = import_session.tasks[0].layer.name - _name = ( - _gs_import_session_info.dataset_name - if import_session.state == enumerations.STATE_COMPLETE - else "" - ) - _alternate = ( - f"{_gs_import_session_info.workspace}:{_gs_import_session_info.dataset_name}" - if import_session.state == enumerations.STATE_COMPLETE - else "" - ) - _to_update = { - "name": _name, - "title": instance.title or _gs_import_session_info.dataset_name, - "workspace": _gs_import_session_info.workspace, - "alternate": _alternate, - "typename": _alternate, - "store": _gs_import_session_info.target_store or _gs_import_session_info.dataset_name, - "subtype": _gs_import_session_info.spatial_files_type.dataset_type, - } - if "defaults" in kwargs: - kwargs["defaults"].update(_to_update) - Dataset.objects.filter(uuid=instance.uuid).update(**_to_update) - instance.get_real_instance_class().objects.filter(uuid=instance.uuid).update(**_to_update) - # Refresh from DB - instance.refresh_from_db() - if kwargs.get("action_type", "create") == "create": - set_styles(instance.get_real_instance(), gs_catalog) - set_attributes_from_geoserver(instance.get_real_instance(), overwrite=True) - elif kwargs.get("action_type", "create") == "create": - logger.exception(Exception(f"Importer Session not valid - STATE: {import_session.state}")) - if import_session.state == enumerations.STATE_COMPLETE: - instance.set_processing_state(enumerations.STATE_PROCESSED) - else: - instance.set_processing_state(import_session.state) - instance.set_dirty_state() - instance.save(notify=False) - except Exception as e: - logger.exception(e) - if kwargs.get("action_type", "create") == "create": - instance.delete() - instance = None - return instance - - def _execute_resource_import( - self, instance, files: list, user, action_type: str, importer_session_opts: typing.Optional[typing.Dict] = None - ): - from geonode.utils import get_allowed_extensions - - ALLOWED_EXTENSIONS = get_allowed_extensions() - - session_opts = dict(importer_session_opts) if importer_session_opts is not None else {} - - spatial_files_type = get_spatial_files_dataset_type(ALLOWED_EXTENSIONS, files) - - if not spatial_files_type: - raise Exception(f"No suitable Spatial Files avaialable for 'ALLOWED_EXTENSIONS' = {ALLOWED_EXTENSIONS}.") - - upload_session, _ = Upload.objects.get_or_create( - resource=instance.get_real_instance().resourcebase_ptr, user=user - ) - upload_session.resource = instance.get_real_instance().resourcebase_ptr - upload_session.save() - - _name = instance.get_real_instance().name - if not _name: - _name = ( - session_opts.get("name", None) or os.path.splitext(os.path.basename(spatial_files_type.base_file))[0] - ) - instance.get_real_instance().name = _name - - gs_dataset = None - try: - gs_dataset = gs_catalog.get_layer(_name) - except Exception as e: - logger.debug(e) - - _workspace = None - _target_store = None - if gs_dataset: - _target_store = gs_dataset.resource.store.name if instance.get_real_instance().subtype == "vector" else None - _workspace = gs_dataset.resource.workspace.name if gs_dataset.resource.workspace else None - - if not _workspace: - _workspace = session_opts.get("workspace", instance.get_real_instance().workspace) - if not _workspace: - _workspace = instance.get_real_instance().workspace or settings.DEFAULT_WORKSPACE - - if not _target_store: - if instance.get_real_instance().subtype == "vector" or spatial_files_type.dataset_type == "vector": - _dsname = ogc_server_settings.datastore_db["NAME"] - _ds = create_geoserver_db_featurestore(store_name=_dsname, workspace=_workspace) - if _ds: - _target_store = session_opts.get("target_store", None) or _dsname - - # opening Import session for the selected layer - # Let's reset the connections first - gs_catalog._cache.clear() - gs_catalog.reset() - # Let's now try the new ingestion - import_session = gs_uploader.start_import(import_id=upload_session.id, name=_name, target_store=_target_store) - - upload_session.set_processing_state(enumerations.STATE_PROCESSED) - upload_session.import_id = import_session.id - upload_session.name = _name - upload_session.complete = True - upload_session.processed = True - upload_session.save() - - _gs_import_session_info = GeoServerImporterSessionInfo( - upload_session=upload_session, - import_session=import_session, - spatial_files_type=spatial_files_type, - dataset_name=None, - workspace=_workspace, - target_store=_target_store, - ) - - import_session.upload_task(files) - task = import_session.tasks[0] - # Changing layer name, mode and target - task.layer.set_target_layer_name(_name) - task.set_update_mode(action_type.upper()) - task.set_target(store_name=_target_store, workspace=_workspace) - transforms = session_opts.get("transforms", None) - if transforms: - task.set_transforms(transforms) - # Starting import process - import_session.commit() - import_session = import_session.reload() - - _gs_import_session_info.import_session = import_session - _gs_import_session_info.dataset_name = import_session.tasks[0].layer.name - - return _gs_import_session_info - def remove_permissions(self, uuid: str, /, instance: ResourceBase = None) -> bool: instance = instance or ResourceManager._get_instance(uuid) diff --git a/geonode/geoserver/tests/test_manager.py b/geonode/geoserver/tests/test_manager.py deleted file mode 100644 index 7d2b9ac2155..00000000000 --- a/geonode/geoserver/tests/test_manager.py +++ /dev/null @@ -1,97 +0,0 @@ -######################################################################### -# -# Copyright (C) 2019 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 os -import base64 -import shutil -from django.test import override_settings -import gisdata -import requests - -from django.conf import settings -from django.contrib.auth import get_user_model - -from geonode import geoserver -from geonode.base import enumerations -from geonode.layers.models import Dataset -from geonode.layers.utils import get_files -from geonode.decorators import on_ogc_backend -from geonode.geoserver.helpers import gs_catalog -from geonode.tests.base import GeoNodeBaseTestSupport -from geonode.geoserver.manager import GeoServerResourceManager -from geonode.base.populate_test_data import create_single_dataset - - -class TestGeoServerResourceManager(GeoNodeBaseTestSupport): - def setUp(self): - self.files = os.path.join(gisdata.GOOD_DATA, "vector/san_andres_y_providencia_water.shp") - self.files_as_dict, self.tmpdir = get_files(self.files) - self.cat = gs_catalog - self.user = get_user_model().objects.get(username="admin") - self.sut = create_single_dataset("san_andres_y_providencia_water.shp") - self.sut.name = "san_andres_y_providencia_water" - self.sut.save() - self.geoserver_url = settings.GEOSERVER_LOCATION - self.geoserver_manager = GeoServerResourceManager() - - def tearDown(self) -> None: - if self.tmpdir: - shutil.rmtree(self.tmpdir, ignore_errors=True) - return super().tearDown() - - @on_ogc_backend(geoserver.BACKEND_PACKAGE) - @override_settings(ASYNC_SIGNALS=False, FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o777, FILE_UPLOAD_PERMISSIONS=0o7777) - def test_revise_resource_value_in_append_should_add_expected_rows_in_the_catalog(self): - layer = Dataset.objects.get(name=self.sut.name) - gs_layer = self.cat.get_layer("san_andres_y_providencia_water") - if gs_layer is None: - _gs_import_session_info = self.geoserver_manager._execute_resource_import( - layer, list(self.files_as_dict.values()), self.user, action_type="create" - ) - _gs_import_session_info = self.geoserver_manager._execute_resource_import( - layer, list(self.files_as_dict.values()), self.user, action_type="append" - ) - basic_auth = base64.b64encode(b"admin:geoserver") - result = requests.get( - f"{self.geoserver_url}/rest/imports/{_gs_import_session_info.import_session.id}", - headers={"Authorization": f"Basic {basic_auth.decode('utf-8')}"}, - ) - self.assertEqual(result.status_code, 200) - self.assertEqual(result.json().get("import").get("state"), enumerations.STATE_COMPLETE) - - @on_ogc_backend(geoserver.BACKEND_PACKAGE) - def test_revise_resource_value_in_replace_should_add_expected_rows_in_the_catalog(self): - layer = Dataset.objects.get(name=self.sut.name) - _gs_import_session_info = self.geoserver_manager._execute_resource_import( - layer, list(self.files_as_dict.values()), self.user, action_type="replace" - ) - basic_auth = base64.b64encode(b"admin:geoserver") - result = requests.get( - f"{self.geoserver_url}/rest/imports/{_gs_import_session_info.import_session.id}", - headers={"Authorization": f"Basic {basic_auth.decode('utf-8')}"}, - ) - self.assertEqual(result.status_code, 200) - self.assertEqual(result.json().get("import").get("state"), enumerations.STATE_COMPLETE) - - @on_ogc_backend(geoserver.BACKEND_PACKAGE) - def test_revise_resource_value_in_replace_should_return_none_for_not_existing_dataset(self): - layer = create_single_dataset("fake_dataset") - _gs_import_session_info = self.geoserver_manager._execute_resource_import( - layer, list(self.files_as_dict.values()), self.user, action_type="replace" - ) - self.assertEqual(_gs_import_session_info.import_session.state, enumerations.STATE_COMPLETE) diff --git a/geonode/geoserver/upload.py b/geonode/geoserver/upload.py deleted file mode 100644 index d5c9ddedfe1..00000000000 --- a/geonode/geoserver/upload.py +++ /dev/null @@ -1,263 +0,0 @@ -######################################################################### -# -# Copyright (C) 2018 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 uuid -import logging -import geoserver - -from geoserver.catalog import ConflictingDataError, UploadError -from geoserver.resource import FeatureType, Coverage - -from django.conf import settings - -from geonode import GeoNodeException -from geonode.layers.utils import dataset_type, get_files -from .helpers import ( - GEOSERVER_LAYER_TYPES, - gs_catalog, - get_store, - get_sld_for, - ogc_server_settings, - _create_db_featurestore, - _create_featurestore, - _create_coveragestore, -) - -logger = logging.getLogger(__name__) - - -def geoserver_dataset_type(filename): - the_type = dataset_type(filename) - return GEOSERVER_LAYER_TYPES[the_type] - - -def geoserver_upload( - dataset, - base_file, - user, - name, - overwrite=True, - title=None, - abstract=None, - permissions=None, - keywords=(), - charset="UTF-8", -): - # Step 2. Check that it is uploading to the same resource type as - # the existing resource - logger.debug( - ">>> Step 2. Make sure we are not trying to overwrite a " "existing resource named [%s] with the wrong type", - name, - ) - the_dataset_type = geoserver_dataset_type(base_file) - - # Get a short handle to the gsconfig geoserver catalog - cat = gs_catalog - - # Ahmed Nour: get workspace by name instead of get default one. - workspace = cat.get_workspace(settings.DEFAULT_WORKSPACE) - # Check if the store exists in geoserver - try: - store = get_store(cat, name, workspace=workspace) - except geoserver.catalog.FailedRequestError: - # There is no store, ergo the road is clear - pass - else: - # If we get a store, we do the following: - resources = cat.get_resources(names=[name], stores=[store], workspaces=[workspace]) - - if len(resources) > 0: - # If our resource is already configured in the store it needs - # to have the right resource type - for resource in resources: - if resource.name == name: - msg = "Name already in use and overwrite is False" - assert overwrite, msg - existing_type = resource.resource_type - if existing_type != the_dataset_type: - msg = ( - f"Type of uploaded file {name} ({the_dataset_type}) " - "does not match type of existing " - f"resource type {existing_type}" - ) - logger.debug(msg) - raise GeoNodeException(msg) - - # Step 3. Identify whether it is vector or raster and which extra files - # are needed. - logger.debug(">>> Step 3. Identifying if [%s] is vector or raster and " "gathering extra files", name) - if the_dataset_type == FeatureType.resource_type: - logger.debug("Uploading vector layer: [%s]", base_file) - if ogc_server_settings.DATASTORE: - create_store_and_resource = _create_db_featurestore - else: - create_store_and_resource = _create_featurestore - elif the_dataset_type == Coverage.resource_type: - logger.debug("Uploading raster layer: [%s]", base_file) - create_store_and_resource = _create_coveragestore - else: - msg = ( - f"The layer type for name {name} is {the_dataset_type}. It should be " - f"{FeatureType.resource_type} or {Coverage.resource_type}," - ) - logger.warn(msg) - raise GeoNodeException(msg) - - # Step 4. Create the store in GeoServer - logger.debug(">>> Step 4. Starting upload of [%s] to GeoServer...", name) - - # Get the helper files if they exist - files, _tmpdir = get_files(base_file) - data = files - if "shp" not in files: - data = base_file - try: - store, gs_resource = create_store_and_resource( - name, data, charset=charset, overwrite=overwrite, workspace=workspace - ) - except UploadError as e: - msg = f"Could not save the layer {name}, there was an upload " f"error: {e}" - logger.warn(msg) - e.args = (msg,) - raise - except ConflictingDataError as e: - # A datastore of this name already exists - msg = ( - f"GeoServer reported a conflict creating a store with name {name}: " - f'"{e}". This should never happen because a brand new name ' - "should have been generated. But since it happened, " - "try renaming the file or deleting the store in GeoServer." - ) - logger.warn(msg) - e.args = (msg,) - raise - except Exception as e: - logger.error("Error during the creation of the resource in GeoServer", exc_info=e) - raise e - - logger.debug(f"The File {name} has been sent to GeoServer without errors.") - - # Step 5. Create the resource in GeoServer - logger.debug(f">>> Step 5. Generating the metadata for {name} after successful import to GeoSever") - - # Verify the resource was created - if not gs_resource: - gs_resource = gs_catalog.get_resource(name=name, workspace=workspace) - - if not gs_resource: - msg = f"GeoNode encountered problems when creating layer {name}.It cannot find the Dataset that matches this Workspace.try renaming your files." - logger.warn(msg) - raise GeoNodeException(msg) - - assert gs_resource.name == name - - # Step 6. Make sure our data always has a valid projection - logger.debug(f">>> Step 6. Making sure [{name}] has a valid projection") - _native_bbox = None - try: - _native_bbox = gs_resource.native_bbox - except Exception: - pass - - if _native_bbox and len(_native_bbox) >= 5 and _native_bbox[4:5][0] == "EPSG:4326": - box = _native_bbox[:4] - minx, maxx, miny, maxy = [float(a) for a in box] - if ( - -180 <= round(minx, 5) <= 180 - and -180 <= round(maxx, 5) <= 180 - and -90 <= round(miny, 5) <= 90 - and -90 <= round(maxy, 5) <= 90 - ): - gs_resource.latlon_bbox = _native_bbox - gs_resource.projection = "EPSG:4326" - else: - logger.warning("BBOX coordinates outside normal EPSG:4326 values for layer " "[%s].", name) - _native_bbox = [-180, -90, 180, 90, "EPSG:4326"] - gs_resource.latlon_bbox = _native_bbox - gs_resource.projection = "EPSG:4326" - logger.debug("BBOX coordinates forced to [-180, -90, 180, 90] for layer [%s].", name) - - # Step 7. Create the style and assign it to the created resource - logger.debug(f">>> Step 7. Creating style for [{name}]") - cat.save(gs_resource) - publishing = cat.get_layer(name) or gs_resource - sld = None - try: - if "sld" in files: - with open(files["sld"], "rb") as f: - sld = f.read() - else: - sld = get_sld_for(cat, dataset) - except Exception as e: - logger.exception(e) - - style = None - if sld: - try: - style = cat.get_style(name, workspace=workspace) - except geoserver.catalog.FailedRequestError: - style = cat.get_style(name) - - try: - overwrite = style or False - cat.create_style(name, sld, overwrite=overwrite, raw=True, workspace=workspace) - cat.reset() - except geoserver.catalog.ConflictingDataError as e: - msg = f"There was already a style named {name}_dataset in GeoServer, " f'try to use: "{e}"' - logger.warn(msg) - e.args = (msg,) - except geoserver.catalog.UploadError as e: - msg = f"Error while trying to upload style named {name}_dataset in GeoServer, " f'try to use: "{e}"' - e.args = (msg,) - logger.exception(e) - - if style is None: - try: - style = cat.get_style(name, workspace=workspace) or cat.get_style(name) - except Exception as e: - style = cat.get_style("point") - msg = f'Could not find any suitable style in GeoServer for Dataset: "{name}"' - e.args = (msg,) - logger.exception(e) - - if style: - publishing.default_style = style - logger.debug("default style set to %s", name) - try: - cat.save(publishing) - except geoserver.catalog.FailedRequestError as e: - msg = f"Error while trying to save resource named {publishing} in GeoServer, " f'try to use: "{e}"' - e.args = (msg,) - logger.exception(e) - - # Step 8. Create the Django record for the layer - logger.debug(">>> Step 8. Creating Django record for [%s]", name) - alternate = f"{workspace.name}:{gs_resource.name}" - dataset_uuid = str(uuid.uuid4()) - - defaults = dict( - store=gs_resource.store.name, - subtype=gs_resource.store.resource_type, - alternate=alternate, - title=title or gs_resource.title, - uuid=dataset_uuid, - abstract=abstract or gs_resource.abstract or "", - owner=user, - ) - - return name, workspace.name, defaults, gs_resource diff --git a/geonode/harvesting/harvesters/base.py b/geonode/harvesting/harvesters/base.py index 8c616046eea..8d2359928cd 100644 --- a/geonode/harvesting/harvesters/base.py +++ b/geonode/harvesting/harvesters/base.py @@ -25,6 +25,8 @@ import typing from pathlib import Path +from deprecated import deprecated + import geonode.upload.files import requests from django.core.files import uploadedfile @@ -176,6 +178,10 @@ def finalize_harvestable_resource_deletion(self, harvestable_resource: "Harvesta return True + @deprecated( + version="4.4.0", + reason="Copy remote datasets/document to local is deprecated. From now on, the configuration will be ignored", + ) def should_copy_resource( self, harvestable_resource: "HarvestableResource", # noqa @@ -243,8 +249,6 @@ def get_geonode_resource_defaults( } if harvested_info.resource_descriptor.identification.lonlat_extent: defaults["ll_bbox_polygon"] = harvested_info.resource_descriptor.identification.lonlat_extent - if self.should_copy_resource(harvestable_resource): - defaults["sourcetype"] = enumerations.SOURCE_TYPE_COPYREMOTE else: defaults["subtype"] = "remote" defaults["sourcetype"] = enumerations.SOURCE_TYPE_REMOTE @@ -297,19 +301,9 @@ def update_geonode_resource( def _create_new_geonode_resource(self, geonode_resource_type, defaults: typing.Dict): logger.debug(f"Creating a new GeoNode resource for resource with uuid: {defaults['uuid']!r}...") resource_defaults = defaults.copy() - resource_files = resource_defaults.pop("files", []) - if len(resource_files) > 0: - logger.debug("calling resource_manager.ingest...") - geonode_resource = resource_manager.ingest( - resource_files, - uuid=resource_defaults["uuid"], - resource_type=geonode_resource_type, - defaults=resource_defaults, - importer_session_opts={"name": resource_defaults["uuid"]}, - ) - else: - logger.debug("calling resource_manager.create...") - geonode_resource = resource_manager.create(resource_defaults["uuid"], geonode_resource_type, defaults) + + logger.debug("calling resource_manager.create...") + geonode_resource = resource_manager.create(resource_defaults["uuid"], geonode_resource_type, defaults) return geonode_resource def _update_existing_geonode_resource(self, geonode_resource: ResourceBase, defaults: typing.Dict): diff --git a/geonode/harvesting/harvesters/geonodeharvester.py b/geonode/harvesting/harvesters/geonodeharvester.py index dad1571b920..24e6b252f66 100644 --- a/geonode/harvesting/harvesters/geonodeharvester.py +++ b/geonode/harvesting/harvesters/geonodeharvester.py @@ -29,6 +29,7 @@ import uuid import dateutil.parser +from deprecated import deprecated import requests from django.contrib.gis import geos from lxml import etree @@ -219,14 +220,15 @@ def get_resource( ) return result + @deprecated( + version="4.4.0", + reason="Copy remote datasets/document to local is deprecated. From now on, the configuration will be ignored", + ) def should_copy_resource( self, harvestable_resource: models.HarvestableResource, ) -> bool: - return { - GeoNodeResourceTypeCurrent.DATASET.value: self.copy_datasets, - GeoNodeResourceTypeCurrent.DOCUMENT.value: self.copy_documents, - }.get(harvestable_resource.remote_resource_type, False) + return False def get_geonode_resource_defaults( self, @@ -236,8 +238,7 @@ def get_geonode_resource_defaults( defaults = super().get_geonode_resource_defaults(harvested_info, harvestable_resource) defaults.update(harvested_info.resource_descriptor.additional_parameters) local_resource_type = self.get_geonode_resource_type(harvestable_resource.remote_resource_type) - to_copy = self.should_copy_resource(harvestable_resource) - if local_resource_type == Document and not to_copy: + if local_resource_type == Document: # since we are not copying the document, we need to provide suitable remote URLs defaults.update( { @@ -247,26 +248,27 @@ def get_geonode_resource_defaults( ) elif local_resource_type == Dataset: defaults.update({"name": harvested_info.resource_descriptor.identification.name}) - if not to_copy: - # since we are not copying the dataset, we need to provide suitable SRID and remote URL - try: - srid = harvested_info.resource_descriptor.reference_systems[0] - except AttributeError: - srid = None - defaults.update( - { - "alternate": defaults["alternate"], - "workspace": defaults["workspace"], - "ows_url": harvested_info.resource_descriptor.distribution.wms_url, - "thumbnail_url": harvested_info.resource_descriptor.distribution.thumbnail_url, - "srid": srid, - "ptype": GXP_PTYPES["GN_WMS"], - "subtype": "remote", - } - ) + # since we are not copying the dataset, we need to provide suitable SRID and remote URL + try: + srid = harvested_info.resource_descriptor.reference_systems[0] + except AttributeError: + srid = None + defaults.update( + { + "alternate": defaults["alternate"], + "workspace": defaults["workspace"], + "ows_url": harvested_info.resource_descriptor.distribution.wms_url, + "thumbnail_url": harvested_info.resource_descriptor.distribution.thumbnail_url, + "srid": srid, + "ptype": GXP_PTYPES["GN_WMS"], + "subtype": "remote", + } + ) return defaults def _get_contact_descriptor(self, role, contact_details: typing.Dict): + if isinstance(contact_details, list): + contact_details = contact_details[0] return resourcedescriptor.RecordDescriptionContact( role=role, name=self._get_related_name(contact_details) or contact_details["username"] ) @@ -593,11 +595,10 @@ def should_copy_resource( self, harvestable_resource: models.HarvestableResource, ) -> bool: - return { - GeoNodeResourceType.DOCUMENT.value: self.copy_documents, - GeoNodeResourceType.DATASET.value: self.copy_datasets, - GeoNodeResourceType.MAP.value: False, - }[harvestable_resource.remote_resource_type] + logger.warning( + "Copy remote datasets/document to local is deprecated. From now on, the configuration will be ignored" + ) + return False def get_geonode_resource_defaults( self, @@ -607,8 +608,8 @@ def get_geonode_resource_defaults( defaults = super().get_geonode_resource_defaults(harvested_info, harvestable_resource) defaults.update(harvested_info.resource_descriptor.additional_parameters) local_resource_type = self.get_geonode_resource_type(harvestable_resource.remote_resource_type) - to_copy = self.should_copy_resource(harvestable_resource) - if local_resource_type == Document and not to_copy: + + if local_resource_type == Document: # since we are not copying the document, we need to provide suitable remote URLs defaults.update( { @@ -622,20 +623,19 @@ def get_geonode_resource_defaults( "name": harvested_info.resource_descriptor.identification.name, } ) - if not to_copy: - # since we are not copying the dataset, we need to provide suitable SRID and remote URL - try: - srid = harvested_info.resource_descriptor.reference_systems[0] - except AttributeError: - srid = None - defaults.update( - { - "name": defaults["name"].rpartition(":")[-1], - "ows_url": harvested_info.resource_descriptor.distribution.wms_url, - "thumbnail_url": harvested_info.resource_descriptor.distribution.thumbnail_url, - "srid": srid, - } - ) + # since we are not copying the dataset, we need to provide suitable SRID and remote URL + try: + srid = harvested_info.resource_descriptor.reference_systems[0] + except AttributeError: + srid = None + defaults.update( + { + "name": defaults["name"].rpartition(":")[-1], + "ows_url": harvested_info.resource_descriptor.distribution.wms_url, + "thumbnail_url": harvested_info.resource_descriptor.distribution.thumbnail_url, + "srid": srid, + } + ) return defaults def _get_num_available_resources_by_type(self) -> typing.Dict[GeoNodeResourceType, int]: @@ -1170,30 +1170,6 @@ def _get_native_format(csw_identification: etree.Element, api_record: typing.Dic return result -def get_spatial_extent_4326(identification_el: etree.Element) -> typing.Optional[geos.Polygon]: - try: - extent_el = identification_el.xpath(".//gmd:extent//gmd:geographicElement", namespaces=identification_el.nsmap)[ - 0 - ] - left_x = get_xpath_value(extent_el, ".//gmd:westBoundLongitude") - right_x = get_xpath_value(extent_el, ".//gmd:eastBoundLongitude") - lower_y = get_xpath_value(extent_el, ".//gmd:southBoundLatitude") - upper_y = get_xpath_value(extent_el, ".//gmd:northBoundLatitude") - # GeoNode seems to have a bug whereby sometimes the reported extent uses a - # comma as the decimal separator, other times it uses a dot - result = geos.Polygon.from_bbox( - ( - float(left_x.replace(",", ".")), - float(lower_y.replace(",", ".")), - float(right_x.replace(",", ".")), - float(upper_y.replace(",", ".")), - ) - ) - except IndexError: - result = None - return result - - def get_spatial_extent_native(api_record: typing.Dict): declared_ewkt = api_record.get("bbox_polygon") if declared_ewkt is not None: @@ -1235,9 +1211,7 @@ def _get_extra_config_schema() -> typing.Dict: "type": "object", "properties": { "harvest_documents": {"type": "boolean", "default": True}, - "copy_documents": {"type": "boolean", "default": False}, "harvest_datasets": {"type": "boolean", "default": True}, - "copy_datasets": {"type": "boolean", "default": False}, "resource_title_filter": { "type": "string", }, @@ -1251,13 +1225,22 @@ def _get_extra_config_schema() -> typing.Dict: def _from_django_record(target_class: typing.Type, record: models.Harvester): + + if ( + "copy_datasets" in record.harvester_type_specific_configuration + or "copy_documents" in record.harvester_type_specific_configuration + ): + logger.warning( + "Copy remote datasets/document to local is deprecated. From now on, the configuration will be ignored" + ) + return target_class( record.remote_url, record.id, harvest_documents=record.harvester_type_specific_configuration.get("harvest_documents", True), harvest_datasets=record.harvester_type_specific_configuration.get("harvest_datasets", True), - copy_datasets=record.harvester_type_specific_configuration.get("copy_datasets", False), - copy_documents=record.harvester_type_specific_configuration.get("copy_documents", False), + copy_datasets=False, + copy_documents=False, resource_title_filter=record.harvester_type_specific_configuration.get("resource_title_filter"), start_date_filter=record.harvester_type_specific_configuration.get("start_date_filter"), end_date_filter=record.harvester_type_specific_configuration.get("end_date_filter"), diff --git a/geonode/harvesting/resourcedescriptor.py b/geonode/harvesting/resourcedescriptor.py index ca9cc82b5c0..8805567bfbc 100644 --- a/geonode/harvesting/resourcedescriptor.py +++ b/geonode/harvesting/resourcedescriptor.py @@ -74,11 +74,6 @@ class RecordDistribution: embed_url: typing.Optional[str] = None -@dataclasses.dataclass() -class MapDescriptorParameters: - last_modified: dt.datetime - - @dataclasses.dataclass() class RecordDescription: uuid: uuid.UUID diff --git a/geonode/harvesting/tasks.py b/geonode/harvesting/tasks.py index 9d025714254..29c472114cc 100644 --- a/geonode/harvesting/tasks.py +++ b/geonode/harvesting/tasks.py @@ -197,10 +197,6 @@ def _harvest_resource(self, harvestable_resource_id: int, harvesting_session_id: harvested_resource_info = worker.get_resource(harvestable_resource) now_ = timezone.now() if harvested_resource_info is not None: - if worker.should_copy_resource(harvestable_resource): - copied_path = worker.copy_resource(harvestable_resource, harvested_resource_info) - if copied_path is not None: - harvested_resource_info.copied_resources.append(copied_path) try: worker.update_geonode_resource( harvested_resource_info, diff --git a/geonode/harvesting/tests/test_harvester_worker_geonode_legacy.py b/geonode/harvesting/tests/test_harvester_worker_geonode_legacy.py index d7bded8b4c6..b235e7dfb9b 100644 --- a/geonode/harvesting/tests/test_harvester_worker_geonode_legacy.py +++ b/geonode/harvesting/tests/test_harvester_worker_geonode_legacy.py @@ -50,7 +50,7 @@ def test_creation_from_harvester(self): { "harvest_documents": True, "harvest_datasets": True, - "copy_datasets": True, + "copy_datasets": False, "copy_documents": True, "resource_title_filter": "something", "start_date_filter": now, diff --git a/geonode/layers/api/exceptions.py b/geonode/layers/api/exceptions.py index 01d3d86f6f8..4bad99363fb 100644 --- a/geonode/layers/api/exceptions.py +++ b/geonode/layers/api/exceptions.py @@ -38,10 +38,3 @@ class InvalidMetadataException(APIException): default_detail = "Input payload is not valid" default_code = "invalid_metadata_exception" category = "dataset_api" - - -class MissingMetadataException(APIException): - status_code = 400 - default_detail = "Metadata is missing" - default_code = "missing_metadata_exception" - category = "dataset_api" diff --git a/geonode/layers/forms.py b/geonode/layers/forms.py index 38db3c7956c..83075ac8b18 100644 --- a/geonode/layers/forms.py +++ b/geonode/layers/forms.py @@ -80,15 +80,6 @@ def __init__(self, *args, **kwargs): ) -class LayerDescriptionForm(forms.Form): - title = forms.CharField(max_length=300, required=True) - abstract = forms.CharField(max_length=2000, widget=forms.Textarea, required=False) - supplemental_information = forms.CharField(max_length=2000, widget=forms.Textarea, required=False) - data_quality_statement = forms.CharField(max_length=2000, widget=forms.Textarea, required=False) - purpose = forms.CharField(max_length=500, required=False) - keywords = forms.CharField(max_length=500, required=False) - - class LayerAttributeForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/geonode/layers/utils.py b/geonode/layers/utils.py index b7102116c9e..2984614361c 100644 --- a/geonode/layers/utils.py +++ b/geonode/layers/utils.py @@ -29,7 +29,6 @@ import logging import tarfile -from osgeo import gdal, osr from zipfile import ZipFile, is_zipfile from random import choice @@ -301,90 +300,6 @@ def is_raster(filename): return False -def get_resolution(filename): - try: - gtif = gdal.Open(filename) - gt = gtif.GetGeoTransform() - __, resx, __, __, __, resy = gt - resolution = f"{resx} {resy}" - return resolution - except Exception: - return None - - -def get_bbox(filename): - """Return bbox in the format [xmin,xmax,ymin,ymax].""" - from django.contrib.gis.gdal import DataSource, SRSException - - srid = 4326 - bbox_x0, bbox_y0, bbox_x1, bbox_y1 = -180, -90, 180, 90 - - try: - if is_vector(filename): - y_min = -90 - y_max = 90 - x_min = -180 - x_max = 180 - datasource = DataSource(filename) - layer = datasource[0] - bbox_x0, bbox_y0, bbox_x1, bbox_y1 = layer.extent.tuple - srs = layer.srs - try: - if not srs: - raise GeoNodeException("Invalid Projection. Dataset is missing CRS!") - srs.identify_epsg() - except SRSException: - pass - epsg_code = srs.srid - # can't find epsg code, then check if bbox is within the 4326 boundary - if epsg_code is None and ( - x_min <= bbox_x0 <= x_max - and x_min <= bbox_x1 <= x_max - and y_min <= bbox_y0 <= y_max - and y_min <= bbox_y1 <= y_max - ): - # set default epsg code - epsg_code = "4326" - elif epsg_code is None: - # otherwise, stop the upload process - raise GeoNodeException("Invalid Datasets. " "Needs an authoritative SRID in its CRS to be accepted") - - # eliminate default EPSG srid as it will be added when this function returned - srid = epsg_code if epsg_code else "4326" - elif is_raster(filename): - gtif = gdal.Open(filename) - gt = gtif.GetGeoTransform() - prj = gtif.GetProjection() - srs = osr.SpatialReference(wkt=prj) - cols = gtif.RasterXSize - rows = gtif.RasterYSize - - ext = [] - xarr = [0, cols] - yarr = [0, rows] - - # Get the extent. - for px in xarr: - for py in yarr: - x = gt[0] + (px * gt[1]) + (py * gt[2]) - y = gt[3] + (px * gt[4]) + (py * gt[5]) - ext.append([x, y]) - - yarr.reverse() - - # ext has four corner points, get a bbox from them. - # order is important, so make sure min and max is correct. - bbox_x0 = min(ext[0][0], ext[2][0]) - bbox_y0 = min(ext[0][1], ext[2][1]) - bbox_x1 = max(ext[0][0], ext[2][0]) - bbox_y1 = max(ext[0][1], ext[2][1]) - srid = srs.GetAuthorityCode(None) if srs else "4326" - except Exception: - pass - - return [bbox_x0, bbox_x1, bbox_y0, bbox_y1, f"EPSG:{str(srid)}"] - - def delete_orphaned_datasets(): """Delete orphaned layer files.""" deleted = [] diff --git a/geonode/layers/views.py b/geonode/layers/views.py index 776085235e0..f7907326936 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -17,7 +17,6 @@ # ######################################################################### import re -import os import json import decimal import logging @@ -70,13 +69,6 @@ logger = logging.getLogger("geonode.layers.views") -DEFAULT_SEARCH_BATCH_SIZE = 10 -MAX_SEARCH_BATCH_SIZE = 25 -GENERIC_UPLOAD_ERROR = _( - "There was an error while attempting to upload your data. \ -Please try again, or contact and administrator if the problem continues." -) - METADATA_UPLOADED_PRESERVE_ERROR = _( "Note: this dataset's orginal metadata was \ populated and preserved by importing a metadata XML file. This metadata cannot be edited." @@ -89,17 +81,6 @@ _PERMISSION_MSG_VIEW = _("You are not permitted to view this dataset") -def log_snippet(log_file): - if not log_file or not os.path.isfile(log_file): - return f"No log file at {log_file}" - - with open(log_file) as f: - f.seek(0, 2) # Seek @ EOF - fsize = f.tell() # Get Size - f.seek(max(fsize - 10024, 0), 0) # Set pos @ last n chars - return f.read() - - def _resolve_dataset(request, alternate, permission="base.view_resourcebase", msg=_PERMISSION_MSG_GENERIC, **kwargs): """ Resolve the layer by the provided typename (which may include service name) and check the optional permission. @@ -449,12 +430,6 @@ def dataset_metadata( layer.regions.add(*new_regions) layer.category = new_category - from geonode.upload.models import Upload - - up_sessions = Upload.objects.filter(resource_id=layer.resourcebase_ptr_id) - if up_sessions.exists() and up_sessions[0].user != layer.owner: - up_sessions.update(user=layer.owner) - dataset_form.save_linked_resources() register_event(request, EventType.EVENT_CHANGE_METADATA, layer) diff --git a/geonode/maps/api/serializers.py b/geonode/maps/api/serializers.py index 228bbd9517d..9ed51769000 100644 --- a/geonode/maps/api/serializers.py +++ b/geonode/maps/api/serializers.py @@ -16,10 +16,9 @@ # along with this program. If not, see . # ######################################################################### -import ast import logging -from dynamic_rest.fields.fields import DynamicField, DynamicRelationField +from dynamic_rest.fields.fields import DynamicRelationField from dynamic_rest.serializers import DynamicModelSerializer from rest_framework import serializers from rest_framework.exceptions import ParseError, ValidationError @@ -38,14 +37,6 @@ logger = logging.getLogger(__name__) -class DynamicListAsStringField(DynamicField): - def to_representation(self, value): - return ast.literal_eval(value) if isinstance(value, str) else value - - def to_internal_value(self, data): - return str(data) - - class DynamicFullyEmbedM2MRelationField(DynamicRelationField): def __init__(self, serializer_class, queryset=None, sideloading=None, debug=False, **kwargs): kwargs["queryset"] = queryset diff --git a/geonode/maps/views.py b/geonode/maps/views.py index dc8a2a538ac..30b91bd6291 100644 --- a/geonode/maps/views.py +++ b/geonode/maps/views.py @@ -506,18 +506,6 @@ def mapdataset_attributes(request, layername): return HttpResponse(json.dumps(layer.attribute_config()), content_type="application/json") -def get_suffix_if_custom(map): - if map.use_custom_template: - if map.featuredurl: - return map.featuredurl - elif map.urlsuffix: - return map.urlsuffix - else: - return None - else: - return None - - def ajax_url_lookup(request): if request.method != "POST": return HttpResponse(content="ajax user lookup requires HTTP POST", status=405, content_type="text/plain") diff --git a/geonode/messaging/consumer.py b/geonode/messaging/consumer.py index 10ce3ee63d3..f634d07ef44 100644 --- a/geonode/messaging/consumer.py +++ b/geonode/messaging/consumer.py @@ -18,7 +18,6 @@ ######################################################################### import logging -import time import json from datetime import datetime @@ -26,7 +25,6 @@ from kombu.mixins import ConsumerMixin from geonode.security.views import send_email_consumer from geonode.layers.views import dataset_view_counter -from geonode.layers.models import Dataset from geonode.geoserver.helpers import gs_slurp from .queues import ( @@ -158,9 +156,6 @@ def on_dataset_viewer(self, body, message): dataset_id = body.get("dataset_id") dataset_view_counter(dataset_id, viewer) - # TODO Disabled for now. This should be handeld through Notifications - # if settings.EMAIL_ENABLE: - # send_email_owner_on_view(owner_dataset, viewer, dataset_id) message.ack() logger.debug("on_dataset_viewer: finished") self._check_message_limit() @@ -190,27 +185,3 @@ def _update_dataset_data(body, last_message): if update_dataset: gs_slurp(True, workspace=workspace, store=store, filter=filter, remove_deleted=True, execute_signals=True) - - -def _wait_for_dataset(dataset_id, num_attempts=5, wait_seconds=1): - """Blocks execution while the Dataset instance is not found on the database - - This is a workaround for the fact that the - ``geonode.geoserver.signals.geoserver_post_save_local`` function might - try to access the layer's ``id`` parameter before the layer is done being - saved in the database. - - """ - - for current in range(1, num_attempts + 1): - try: - instance = Dataset.objects.get(id=dataset_id) - logger.debug(f"Attempt {current}/{num_attempts} - Found layer in the " "database") - break - except Dataset.DoesNotExist: - time.sleep(wait_seconds) - logger.debug(f"Attempt {current}/{num_attempts} - Could not find layer " "instance") - else: - logger.debug(f"Reached maximum attempts and layer {dataset_id} is still not " "saved. Exiting...") - raise Dataset.DoesNotExist - return instance diff --git a/geonode/messaging/producer.py b/geonode/messaging/producer.py index cb69bf9b928..31a33abf587 100644 --- a/geonode/messaging/producer.py +++ b/geonode/messaging/producer.py @@ -23,7 +23,7 @@ from decorator import decorator from kombu import BrokerConnection from kombu.common import maybe_declare -from .queues import queue_email_events, queue_geoserver_events, queue_notifications_events, queue_dataset_viewers +from .queues import queue_email_events, queue_notifications_events, queue_dataset_viewers from geonode.messaging.apps import url, producers, connection, broker_socket_timeout, task_serializer from .consumer import Consumer @@ -83,20 +83,6 @@ def send_email_producer(dataset_uuid, user_id): producer.release() -@sync_if_local_memory -def geoserver_upload_dataset(payload): - with producers[connection].acquire(block=True, timeout=broker_socket_timeout) as producer: - maybe_declare(queue_geoserver_events, producer.channel) - producer.publish( - payload, - exchange="geonode", - serializer=task_serializer, - routing_key="geonode.geoserver", - timeout=broker_socket_timeout, - ) - producer.release() - - @sync_if_local_memory def notifications_send(payload, created=None): with producers[connection].acquire(block=True, timeout=broker_socket_timeout) as producer: diff --git a/geonode/monitoring/service_handlers.py b/geonode/monitoring/service_handlers.py index 99dd6da37e6..3294a02e6ac 100644 --- a/geonode/monitoring/service_handlers.py +++ b/geonode/monitoring/service_handlers.py @@ -244,7 +244,3 @@ def get_for_service(sname): HostGeoNodeServiceExpose, ) } - - -def exposes_for_service(sname): - return exposes[sname] diff --git a/geonode/notifications_helper.py b/geonode/notifications_helper.py index 0ee0a1a1981..2c9d684d47a 100644 --- a/geonode/notifications_helper.py +++ b/geonode/notifications_helper.py @@ -75,15 +75,6 @@ def wrap(*args, **kwargs): return wrap -def send_now_notification(*args, **kwargs): - """ - Simple wrapper around notifications.model send(). - This can be called safely if notifications are not installed. - """ - if has_notifications: - return notifications.models.send_now(*args, **kwargs) - - @call_celery def send_notification(*args, **kwargs): """ diff --git a/geonode/people/profileextractors.py b/geonode/people/profileextractors.py index f6022364c97..93dc52cca0c 100644 --- a/geonode/people/profileextractors.py +++ b/geonode/people/profileextractors.py @@ -195,11 +195,6 @@ def extract_roles(self, data): return data.get("roles", "") -def _get_latest_position(data): - all_positions = data.get("positions", {"values": []})["values"] - return all_positions[0] if any(all_positions) else None - - class OpenIDGroupRoleMapper: """GeoNode will look for names like: ["GroupProfile1.Role", "GroupProfile2.Role", ..., "GroupProfileN.Role"]""" diff --git a/geonode/people/tests.py b/geonode/people/tests.py index 9b1d51f05fc..72e13207e0b 100644 --- a/geonode/people/tests.py +++ b/geonode/people/tests.py @@ -79,7 +79,7 @@ def test_admin_only_access(self): """ self.client.logout() response = self.client.get(reverse("set_user_dataset_permissions")) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 403) @patch("geonode.base.views.UserAndGroupPermissionsForm.is_valid") @patch("geonode.base.views.UserAndGroupPermissionsForm.errors", new_callable=PropertyMock) diff --git a/geonode/proxy/tests.py b/geonode/proxy/tests.py index 3b04ec080f4..a1b520c0193 100644 --- a/geonode/proxy/tests.py +++ b/geonode/proxy/tests.py @@ -36,8 +36,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile from unittest.mock import patch -from geonode.upload.models import Upload - try: from unittest.mock import MagicMock except ImportError: @@ -287,6 +285,9 @@ def test_download_url_with_existing_files(self, fopen, fexists): dataset_files = [ f"{settings.PROJECT_ROOT}/assets/tests/data/one.json", ] + asset, link = create_asset_and_link( + dataset, get_user_model().objects.get(username="admin"), dataset_files, clone_files=False + ) asset, link = create_asset_and_link( dataset, get_user_model().objects.get(username="admin"), dataset_files, clone_files=False @@ -296,10 +297,6 @@ def test_download_url_with_existing_files(self, fopen, fexists): dataset.refresh_from_db() - upload = Upload.objects.create(state="RUNNING", resource=dataset) - - assert upload - self.client.login(username="admin", password="admin") # ... all should be good response = self.client.get(reverse("download", args=(dataset.id,))) @@ -322,6 +319,9 @@ def test_download_files(self, fopen, fexists): dataset_files = [ f"{settings.PROJECT_ROOT}/assets/tests/data/one.json", ] + asset, link = create_asset_and_link( + dataset, get_user_model().objects.get(username="admin"), dataset_files, clone_files=False + ) asset, link = create_asset_and_link( dataset, get_user_model().objects.get(username="admin"), dataset_files, clone_files=False @@ -331,8 +331,6 @@ def test_download_files(self, fopen, fexists): dataset.refresh_from_db() - Upload.objects.create(state="COMPLETE", resource=dataset) - self.client.login(username="admin", password="admin") response = self.client.get(reverse("download", args=(dataset.id,))) # headers and status assertions @@ -389,16 +387,11 @@ def test_tag_original_link_available_with_different_netlock_should_return_true(s self.assertTrue(actual) def test_should_return_false_if_no_files_are_available(self): - _ = Upload.objects.create(state="RUNNING", resource=self.resource) - actual = original_link_available(self.context, self.resource.resourcebase_ptr_id, self.url) self.assertFalse(actual) @patch("geonode.storage.manager.storage_manager.exists", return_value=True) def test_should_return_true_if_files_are_available(self, fexists): - upload = Upload.objects.create(state="RUNNING", resource=self.resource) - - assert upload dataset_files = [ "/tmpe1exb9e9/foo_file.dbf", diff --git a/geonode/proxy/views.py b/geonode/proxy/views.py index fd593728bee..9f883ad9bbe 100644 --- a/geonode/proxy/views.py +++ b/geonode/proxy/views.py @@ -36,7 +36,6 @@ from django.views.decorators.csrf import requires_csrf_token from geonode.layers.models import Dataset -from geonode.upload.models import Upload from geonode.base.models import ResourceBase from geonode.services.models import Service from geonode.storage.manager import storage_manager @@ -286,7 +285,7 @@ def download(request, resourceid, sender=Dataset): "Last-Modified": zs.last_modified, }, ) - except (NotImplementedError, Upload.DoesNotExist): + except NotImplementedError: traceback.print_exc() tb = traceback.format_exc() logger.debug(tb) diff --git a/geonode/resource/api/views.py b/geonode/resource/api/views.py index c99c11c51b9..29ff254156d 100644 --- a/geonode/resource/api/views.py +++ b/geonode/resource/api/views.py @@ -16,7 +16,6 @@ # along with this program. If not, see . # ######################################################################### -import json import logging from dynamic_rest.filters import DynamicFilterBackend, DynamicSortingFilter @@ -26,8 +25,6 @@ from geonode.base.api.permissions import IsOwnerOrReadOnly from geonode.resource.api.exceptions import ExecutionRequestException from geonode.resource.api.serializer import ExecutionRequestSerializer -from geonode.resource.manager import resource_manager -from geonode.security.utils import get_resources_with_perms from oauth2_provider.contrib.rest_framework import OAuth2Authentication from rest_framework import status from rest_framework.exceptions import NotFound @@ -40,57 +37,10 @@ from rest_framework.viewsets import GenericViewSet from ..models import ExecutionRequest -from .utils import filtered, resolve_type_serializer logger = logging.getLogger(__name__) -@api_view(["GET"]) -def resource_service_search(request, resource_type: str = None): - """ - Return a list of resources matching the filtering criteria. - - Sample requests: - - - Get all ResourceBases the user has access to: - http://localhost:8000/api/v2/resource-service/search - - - Get all ResourceBases filtered by type criteria the user has access to: - http://localhost:8000/api/v2/resource-service/search/layer - - - Get ResourceBases filtered by model criteria the user has access to: - http://localhost:8000/api/v2/resource-service/search/?filter={"title__icontains":"foo"} - - """ - try: - search_filter = json.loads(request.GET.get("filter", "{}")) - except Exception as e: - return Response(status=status.HTTP_400_BAD_REQUEST, exception=e) - - _resource_type, _serializer = resolve_type_serializer(resource_type) - logger.error(f"Serializing '{_resource_type}' resources through {_serializer}.") - return filtered(request, resource_manager.search(search_filter, resource_type=_resource_type), _serializer) - - -@api_view(["GET"]) -def resource_service_exists(request, uuid: str): - """ - Returns a JSON boolean success field valorized with the 'exists' operation outcome. - - - GET http://localhost:8000/api/v2/resource-service/exists/13decd74-df04-11eb-a0c1-00155dc3de71 - ``` - 200, - { - 'success': true - } - ``` - """ - _exists = False - if resource_manager.exists(uuid): - _exists = get_resources_with_perms(request.user).filter(uuid=uuid).exists() - return Response({"success": _exists}, status=status.HTTP_200_OK) - - @api_view(["GET"]) def resource_service_execution_status(request, execution_id: str): """Main dispatcher endpoint to follow an API request status progress @@ -138,6 +88,9 @@ class ExecutionRequestViewset(WithDynamicViewSetMixin, ListModelMixin, RetrieveM pagination_class = GeoNodeApiPagination http_method_names = ["get", "delete"] + class Meta: + ordering = ["created"] + def get_queryset(self, queryset=None): return ExecutionRequest.objects.filter(user=self.request.user).order_by("pk") diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index c99f4995ac7..f5322a46c39 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -37,7 +37,7 @@ from django.contrib.auth.models import Permission from django.utils.module_loading import import_string from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist, ValidationError, FieldDoesNotExist +from django.core.exceptions import ValidationError, FieldDoesNotExist from geonode.base.models import ResourceBase, LinkedResource @@ -128,61 +128,6 @@ def update( """ pass - @abstractmethod - def ingest( - self, - files: typing.List[str], - /, - uuid: str = None, - resource_type: typing.Optional[object] = None, - defaults: dict = {}, - **kwargs, - ) -> ResourceBase: - """The method allows to create a resource by providing the list of files. - - e.g.: - In [1]: from geonode.resource.manager import resource_manager - - In [2]: from geonode.layers.models import Dataset - - In [3]: from django.contrib.auth import get_user_model - - In [4]: admin = get_user_model().objects.get(username='admin') - - In [5]: files = ["/.../san_andres_y_providencia_administrative.dbf", "/.../san_andres_y_providencia_administrative.prj", - ...: "/.../san_andres_y_providencia_administrative.shx", "/.../san_andres_y_providencia_administrative.sld", "/.../san_andres_y_providencia_administrative.shp"] - - In [6]: resource_manager.ingest(files, resource_type=Dataset, defaults={'owner': admin}) - """ - pass - - @abstractmethod - def copy( - self, instance: ResourceBase, /, uuid: str = None, owner: settings.AUTH_USER_MODEL = None, defaults: dict = {} - ) -> ResourceBase: - """The method makes a copy of the existing resource. - - - It makes a copy of the files - - It creates a new layer on the GIS backend in the case the ResourceType is a Dataset - """ - pass - - @abstractmethod - def append(self, instance: ResourceBase, vals: dict = {}) -> ResourceBase: - """The method appends data to an existing resource. - - - It assumes any GIS backend resource (e.g. layers on GeoServer) already exist. - """ - pass - - @abstractmethod - def replace(self, instance: ResourceBase, vals: dict = {}) -> ResourceBase: - """The method replaces data of an existing resource. - - - It assumes any GIS backend resource (e.g. layers on GeoServer) already exist. - """ - pass - @abstractmethod def exec(self, method: str, uuid: str, /, instance: ResourceBase = None, **kwargs) -> ResourceBase: """A generic 'exec' method allowing to invoke specific methods of the concrete resource manager not exposed by the interface. @@ -448,58 +393,6 @@ def update( _resource.clear_dirty_state() return _resource - def ingest( - self, - files: typing.List[str], - /, - uuid: str = None, - resource_type: typing.Optional[object] = None, - defaults: dict = {}, - **kwargs, - ) -> ResourceBase: - instance = None - to_update = defaults.copy() - to_update_with_files = {**to_update, **{"files": files}} - try: - with transaction.atomic(): - if resource_type == Document: - if "name" in to_update: - to_update.pop("name") - instance = self.create(uuid, resource_type=Document, defaults=to_update_with_files) - elif resource_type == Dataset: - if files: - instance = self.create(uuid, resource_type=Dataset, defaults=to_update_with_files) - else: - logger.warning(f"Will not create a Dataset without any file. Values: {defaults}") - - if instance: - instance = self._concrete_resource_manager.ingest( - storage_manager.copy_files_list(files), - uuid=instance.uuid, - resource_type=resource_type, - defaults=to_update, - **kwargs, - ) - instance.set_processing_state(enumerations.STATE_PROCESSED) - instance.save(notify=False) - except Exception as e: - logger.exception(e) - if instance: - instance.set_processing_state(enumerations.STATE_INVALID) - if instance: - try: - resourcebase_post_save(instance.get_real_instance()) - # Finalize Upload - if "user" in to_update: - to_update.pop("user") - instance = self.update(instance.uuid, instance=instance, vals=to_update) - self.set_thumbnail(instance.uuid, instance=instance) - except Exception as e: - logger.exception(e) - finally: - instance.clear_dirty_state() - return instance - def copy( self, instance: ResourceBase, /, uuid: str = None, owner: settings.AUTH_USER_MODEL = None, defaults: dict = {} ) -> ResourceBase: @@ -552,7 +445,14 @@ def copy( if files: to_update = {"files": files} - _resource = self._concrete_resource_manager.copy(instance, uuid=_resource.uuid, defaults=to_update) + assets_and_links = copy_assets_and_links(instance, target=_resource) + # we're just merging all the files together: it won't work once we have multiple assets per resource + # TODO: get the files from the proper Asset, or make the _concrete_resource_manager.copy use assets + to_update = {} + + files = list(itertools.chain.from_iterable([asset.location for asset, _ in assets_and_links])) + if files: + to_update = {"files": files} except Exception as e: logger.exception(e) @@ -572,47 +472,6 @@ def copy( _resource.set_processing_state(enumerations.STATE_PROCESSED) return _resource - def append(self, instance: ResourceBase, vals: dict = {}, *args, **kwargs): - if self._validate_resource(instance.get_real_instance(), "append"): - self._concrete_resource_manager.append(instance.get_real_instance(), vals=vals) - to_update = vals.copy() - if instance: - if "user" in to_update: - to_update.pop("user") - return self.update(instance.uuid, instance.get_real_instance(), vals=to_update, *args, **kwargs) - return instance - - def replace(self, instance: ResourceBase, vals: dict = {}, *args, **kwargs): - if self._validate_resource(instance.get_real_instance(), "replace"): - if vals.get("files", None) and kwargs.get("store_spatial_files", True): - vals.update(storage_manager.replace(instance.get_real_instance(), vals.get("files"))) - self._concrete_resource_manager.replace(instance.get_real_instance(), vals=vals, *args, **kwargs) - to_update = vals.copy() - if instance: - if "user" in to_update: - to_update.pop("user") - return self.update(instance.uuid, instance.get_real_instance(), vals=to_update, *args, **kwargs) - return instance - - def _validate_resource(self, instance: ResourceBase, action_type: str) -> bool: - if not isinstance(instance, Dataset) and action_type == "append": - raise Exception("Append data is available only for Layers") - - if isinstance(instance, Document) and action_type == "replace": - return True - - exists = self._concrete_resource_manager.exists(instance.uuid, instance) - - if exists and action_type == "append": - if isinstance(instance, Dataset): - if instance.is_vector(): - is_valid = True - elif exists and action_type == "replace": - is_valid = True - else: - raise ObjectDoesNotExist("Resource does not exists") - return is_valid - @transaction.atomic def exec(self, method: str, uuid: str, /, instance: ResourceBase = None, **kwargs) -> ResourceBase: _resource = instance or ResourceManager._get_instance(uuid) diff --git a/geonode/resource/tests.py b/geonode/resource/tests.py index 326f2b24209..e8cf11572a4 100644 --- a/geonode/resource/tests.py +++ b/geonode/resource/tests.py @@ -23,7 +23,6 @@ from unittest.mock import patch from django.contrib.auth import get_user_model -from django.core.exceptions import ObjectDoesNotExist from geonode.groups.models import GroupProfile from geonode.base.populate_test_data import create_models @@ -120,15 +119,22 @@ def test_update(self): def test_ingest(self): dt_files = [os.path.join(GOOD_DATA, "raster", "relief_san_andres.tif")] - defaults = {"owner": self.user} # raises an exception if resource_type is not provided - self.rm.ingest(dt_files) + + res = self.rm.create( + None, + resource_type=Document, + defaults=dict(owner=self.user, files=dt_files), + ) # ingest with documents - res = self.rm.ingest(dt_files, resource_type=Document, defaults=defaults) self.assertTrue(isinstance(res, Document)) res.delete() # ingest with datasets - res = self.rm.ingest(dt_files, resource_type=Dataset, defaults=defaults) + res = self.rm.create( + None, + resource_type=Dataset, + defaults=dict(owner=self.user, files=dt_files), + ) self.assertTrue(isinstance(res, Dataset)) res.delete() @@ -149,8 +155,8 @@ def _copy_assert_resource(res, title): dt_files = [os.path.join(GOOD_DATA, "raster", "relief_san_andres.tif")] # copy with documents - res = self.rm.ingest( - dt_files, + res = self.rm.create( + None, resource_type=Document, defaults={ "title": "relief_san_andres", @@ -158,20 +164,24 @@ def _copy_assert_resource(res, title): "extension": "tif", "data_title": "relief_san_andres", "data_type": "tif", + "files": dt_files, }, ) + self.assertTrue(isinstance(res, Document)) _copy_assert_resource(res, "Testing Document 2") # copy with datasets - res = self.rm.ingest( - dt_files, + # copy with documents + res = self.rm.create( + None, resource_type=Dataset, defaults={ "owner": self.user, "title": "Testing Dataset", "data_title": "relief_san_andres", "data_type": "tif", + "files": dt_files, }, ) self.assertTrue(isinstance(res, Dataset)) @@ -202,45 +212,6 @@ def _copy_assert_resource(res, title): self.assertTrue(isinstance(res, Map)) _copy_assert_resource(res, "A Test Map 2") - @patch.object(ResourceManager, "_validate_resource") - def test_append(self, mock_validator): - mock_validator.return_value = True - dt = create_single_dataset("test_append_dataset") - # Before append - self.assertEqual(dt.name, "test_append_dataset") - # After append - self.rm.append(dt, vals={"name": "new_name_test_append_dataset"}) - self.assertEqual(dt.name, "new_name_test_append_dataset") - # test with failing validator - mock_validator.return_value = False - self.rm.append(dt, vals={"name": "new_name2"}) - self.assertEqual(dt.name, "new_name_test_append_dataset") - - @patch.object(ResourceManager, "_validate_resource") - def test_replace(self, mock_validator): - dt = create_single_dataset("test_replace_dataset") - mock_validator.return_value = True - self.rm.replace(dt, vals={"name": "new_name_test_replace_dataset"}) - self.assertEqual(dt.name, "new_name_test_replace_dataset") - # test with failing validator - mock_validator.return_value = False - self.rm.replace(dt, vals={"name": "new_name2"}) - self.assertEqual(dt.name, "new_name_test_replace_dataset") - - def test_validate_resource(self): - doc = create_single_doc("test_delete_doc") - dt = create_single_dataset("test_delete_dataset") - map = create_single_map("test_delete_dataset") - with self.assertRaises(Exception): - # append is for only datasets - self.rm._validate_resource(doc, action_type="append") - self.assertTrue(self.rm._validate_resource(doc, action_type="replace")) - self.assertTrue(self.rm._validate_resource(dt, action_type="replace")) - self.assertTrue(self.rm._validate_resource(map, action_type="replace")) - with self.assertRaises(ObjectDoesNotExist): - # TODO In function rais this only when object is not found - self.rm._validate_resource(dt, action_type="invalid") - def test_exec(self): map = create_single_map("test_exec_map") self.assertIsNone(self.rm.exec("set_style", None, instance=None)) diff --git a/geonode/security/tests.py b/geonode/security/tests.py index 77bee1fcfcc..d2707531bad 100644 --- a/geonode/security/tests.py +++ b/geonode/security/tests.py @@ -20,12 +20,12 @@ import json import base64 import logging +from unittest.mock import patch import uuid import os import requests import importlib import mock -import gisdata from requests.auth import HTTPBasicAuth from tastypie.test import ResourceTestCaseMixin @@ -42,13 +42,12 @@ from guardian.shortcuts import assign_perm, get_anonymous_user from geonode import geoserver -from geonode.geoserver.helpers import geofence, gf_utils, gs_catalog -from geonode.geoserver.manager import GeoServerResourceManager -from geonode.layers.utils import get_files +from geonode.geoserver.helpers import geofence, gf_utils from geonode.maps.models import Map from geonode.layers.models import Dataset from geonode.documents.models import Document from geonode.compat import ensure_string +from geonode.upload.models import ResourceHandlerInfo from geonode.utils import check_ogc_backend from geonode.tests.utils import check_dataset from geonode.decorators import on_ogc_backend @@ -743,22 +742,23 @@ def test_perm_specs_synchronization(self): rules_count = geofence.get_rules_count() self.assertEqual(rules_count, 0) + @patch.dict(os.environ, {"ASYNC_SIGNALS": "False"}) + @override_settings(ASYNC_SIGNALS=False) @on_ogc_backend(geoserver.BACKEND_PACKAGE) def test_dataset_permissions(self): # Test permissions on a layer - files = os.path.join(gisdata.GOOD_DATA, "vector/san_andres_y_providencia_poi.shp") - files_as_dict, self.tmpdir = get_files(files) + from geonode.upload import project_dir bobby = get_user_model().objects.get(username="bobby") - layer = create_single_dataset( - "san_andres_y_providencia_poi", - { - "owner": self.user, - "title": "Testing Dataset", - "data_title": "relief_san_andres", - "data_type": "tif", - }, - ) + + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": open(f"{project_dir}/tests/fixture/valid.geojson", "rb"), + } + response = self.client.post(reverse("importer_upload"), data=payload) + layer = ResourceHandlerInfo.objects.filter(execution_request=response.json()["execution_id"]).first().resource + if layer is None: + raise Exception("error during import") layer = resource_manager.update( layer.uuid, instance=layer, notify=False, vals=dict(owner=bobby, workspace=settings.DEFAULT_WORKSPACE) ) @@ -767,14 +767,14 @@ def test_dataset_permissions(self): self.assertIsNotNone(layer.ows_url) self.assertIsNotNone(layer.ptype) self.assertIsNotNone(layer.sourcetype) - self.assertEqual(layer.alternate, "geonode:san_andres_y_providencia_poi") + self.assertEqual(layer.alternate, "geonode:valid") # Reset GeoFence Rules delete_all_geofence_rules() rules_count = geofence.get_rules_count() self.assertEqual(rules_count, 0) - layer = Dataset.objects.get(name="san_andres_y_providencia_poi") + layer = Dataset.objects.get(name="valid") # removing duplicates while Dataset.objects.filter(alternate=layer.alternate).count() > 1: Dataset.objects.filter(alternate=layer.alternate).last().delete() @@ -789,15 +789,6 @@ def test_dataset_permissions(self): perm_spec = {"users": {"AnonymousUser": []}, "groups": []} layer.set_permissions(perm_spec) - gs_layer = gs_catalog.get_layer("3Asan_andres_y_providencia_poi") - if gs_layer is None: - GeoServerResourceManager()._execute_resource_import( - layer, - list(files_as_dict.values()), - get_user_model().objects.get(username="admin"), - action_type="create", - ) - url = ( f"{settings.GEOSERVER_LOCATION}ows?" "LAYERS=geonode%3Asan_andres_y_providencia_poi&STYLES=" @@ -882,6 +873,8 @@ def test_dataset_permissions(self): delete_all_geofence_rules() rules_count = geofence.get_rules_count() self.assertTrue(rules_count == 0) + if layer: + layer.delete() def test_maplayers_default_permissions(self): """Verify that Dataset.set_default_permissions is behaving as expected""" diff --git a/geonode/security/views.py b/geonode/security/views.py index b5792caad83..6cf654d59d8 100644 --- a/geonode/security/views.py +++ b/geonode/security/views.py @@ -382,32 +382,3 @@ def send_email_consumer(dataset_uuid, user_id): resource = get_object_or_404(ResourceBase, uuid=dataset_uuid) user = get_user_model().objects.get(id=user_id) send_notification([resource.owner], "request_download_resourcebase", {"resource": resource, "from_user": user}) - - -def send_email_owner_on_view(owner, viewer, dataset_id, geonode_email="email@geo.node"): - # get owner and viewer emails - owner_email = get_user_model().objects.get(username=owner).email - layer = Dataset.objects.get(id=dataset_id) - # check if those values are empty - if owner_email and geonode_email: - from django.core.mail import EmailMessage - - # TODO: Copy edit message. - subject_email = "Your Dataset has been seen." - msg = f"Your layer called {layer.name} with uuid={layer.uuid}" f" was seen by {viewer}" - try: - email = EmailMessage( - subject=subject_email, - body=msg, - from_email=geonode_email, - to=[ - owner_email, - ], - reply_to=[ - geonode_email, - ], - ) - email.content_subtype = "html" - email.send() - except Exception: - traceback.print_exc() diff --git a/geonode/services/tests.py b/geonode/services/tests.py index 1da5649bc08..f119add0f4a 100644 --- a/geonode/services/tests.py +++ b/geonode/services/tests.py @@ -29,10 +29,8 @@ from arcrest import MapService as ArcMapService from unittest import TestCase as StandardTestCase from owslib.wms import WebMapService as OwsWebMapService - from django.test import Client, override_settings from django.urls import reverse -from django.db.utils import IntegrityError from django.contrib.auth import get_user_model from django.template.defaultfilters import slugify from django.contrib.staticfiles.testing import StaticLiveServerTestCase @@ -826,7 +824,7 @@ def test_add_duplicate_remote_service_url(self): # Try adding the same URL again form = forms.CreateServiceForm(form_data) self.assertEqual(Service.objects.count(), 1) - with self.assertRaises(IntegrityError): + with self.assertRaises(KeyError): self.client.post(reverse("register_service"), data=form_data) self.assertEqual(Service.objects.count(), 1) diff --git a/geonode/services/utils.py b/geonode/services/utils.py index ba2b9649ed6..1485b9b1520 100644 --- a/geonode/services/utils.py +++ b/geonode/services/utils.py @@ -25,39 +25,6 @@ logger = logging.getLogger(__name__) -def flip_coordinates(c1, c2): - if c1 > c2: - logger.debug(f"Flipping coordinates {c1}, {c2}") - temp = c1 - c1 = c2 - c2 = temp - return c1, c2 - - -def format_float(value): - if value is None: - return None - try: - value = float(value) - if value > 999999999: - return None - return value - except ValueError: - return None - - -def bbox2wktpolygon(bbox): - """ - Return OGC WKT Polygon of a simple bbox list of strings - """ - - minx = float(bbox[0]) - miny = float(bbox[1]) - maxx = float(bbox[2]) - maxy = float(bbox[3]) - return f"POLYGON(({minx:.2f} {miny:.2f}, {minx:.2f} {maxy:.2f}, {maxx:.2f} {maxy:.2f}, {maxx:.2f} {miny:.2f}, {minx:.2f} {miny:.2f}))" - - def inverse_mercator(xy): """ Given coordinates in spherical mercator, return a lon,lat tuple. @@ -68,12 +35,6 @@ def inverse_mercator(xy): return (lon, lat) -def mercator_to_llbbox(bbox): - minlonlat = inverse_mercator([bbox[0], bbox[1]]) - maxlonlat = inverse_mercator([bbox[2], bbox[3]]) - return [minlonlat[0], minlonlat[1], maxlonlat[0], maxlonlat[1]] - - def get_esri_service_name(url): """ A method to get a service name from an esri endpoint. diff --git a/geonode/settings.py b/geonode/settings.py index 119844bbe02..03807b16dbc 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -711,6 +711,9 @@ "geonode": { "level": "WARN", }, + "importer": { + "level": "INFO", + }, "geonode.br": {"level": "INFO", "handlers": ["br"], "propagate": False}, "geoserver-restconfig.catalog": { "level": "ERROR", @@ -1078,13 +1081,7 @@ DEFAULT_BACKEND_UPLOADER = {'geonode.importer'} """ UPLOADER = { - "BACKEND": os.getenv("DEFAULT_BACKEND_UPLOADER", "geonode.importer"), - "OPTIONS": { - "TIME_ENABLED": ast.literal_eval(os.getenv("TIME_ENABLED", "False")), - "MOSAIC_ENABLED": ast.literal_eval(os.getenv("MOSAIC_ENABLED", "False")), - }, - "SUPPORTED_CRS": ["EPSG:4326", "EPSG:3785", "EPSG:3857", "EPSG:32647", "EPSG:32736"], - "SUPPORTED_EXT": [".shp", ".csv", ".kml", ".kmz", ".json", ".geojson", ".tif", ".tiff", ".geotiff", ".gml", ".xml"], + "BACKEND": os.getenv("DEFAULT_BACKEND_UPLOADER", "geonode.upload"), } EPSG_CODE_MATCHES = { @@ -2157,8 +2154,6 @@ def get_geonode_catalogue_service(): ] UI_REQUIRED_FIELDS = ast.literal_eval(os.getenv("UI_REQUIRED_FIELDS ", "[]")) -UPLOAD_SESSION_EXPIRY_HOURS = os.getenv("UPLOAD_SESSION_EXPIRY_HOURS ", 24) - # If a command name is listed here, the command will be available to admins over http # This list is used by the management_commands_http app MANAGEMENT_COMMANDS_EXPOSED_OVER_HTTP = set( @@ -2241,7 +2236,7 @@ def get_geonode_catalogue_service(): """ SIZE_RESTRICTED_FILE_UPLOAD_ELEGIBLE_URL_NAMES = ( "data_upload", - "uploads-upload", + "importer_upload", "document_upload", ) @@ -2297,42 +2292,60 @@ def get_geonode_catalogue_service(): ] INSTALLED_APPS += ( "dynamic_models", - "importer", - "importer.handlers", + # "importer", + "geonode.upload.handlers", ) CELERY_TASK_QUEUES += ( - Queue("importer.import_orchestrator", GEONODE_EXCHANGE, routing_key="importer.import_orchestrator"), - Queue("importer.import_resource", GEONODE_EXCHANGE, routing_key="importer.import_resource", max_priority=8), - Queue("importer.publish_resource", GEONODE_EXCHANGE, routing_key="importer.publish_resource", max_priority=8), + Queue("geonode.upload.import_orchestrator", GEONODE_EXCHANGE, routing_key="geonode.upload.import_orchestrator"), + Queue( + "geonode.upload.import_resource", GEONODE_EXCHANGE, routing_key="geonode.upload.import_resource", max_priority=8 + ), + Queue( + "geonode.upload.publish_resource", + GEONODE_EXCHANGE, + routing_key="geonode.upload.publish_resource", + max_priority=8, + ), Queue( - "importer.create_geonode_resource", + "geonode.upload.create_geonode_resource", GEONODE_EXCHANGE, - routing_key="importer.create_geonode_resource", + routing_key="geonode.upload.create_geonode_resource", max_priority=8, ), Queue( - "importer.import_with_ogr2ogr", GEONODE_EXCHANGE, routing_key="importer.import_with_ogr2ogr", max_priority=10 + "geonode.upload.import_with_ogr2ogr", + GEONODE_EXCHANGE, + routing_key="geonode.upload.import_with_ogr2ogr", + max_priority=10, + ), + Queue( + "geonode.upload.import_next_step", + GEONODE_EXCHANGE, + routing_key="geonode.upload.import_next_step", + max_priority=3, ), - Queue("importer.import_next_step", GEONODE_EXCHANGE, routing_key="importer.import_next_step", max_priority=3), Queue( - "importer.create_dynamic_structure", + "geonode.upload.create_dynamic_structure", GEONODE_EXCHANGE, - routing_key="importer.create_dynamic_structure", + routing_key="geonode.upload.create_dynamic_structure", max_priority=10, ), Queue( - "importer.copy_geonode_resource", GEONODE_EXCHANGE, routing_key="importer.copy_geonode_resource", max_priority=0 + "geonode.upload.copy_geonode_resource", + GEONODE_EXCHANGE, + routing_key="geonode.upload.copy_geonode_resource", + max_priority=0, + ), + Queue("geonode.upload.copy_dynamic_model", GEONODE_EXCHANGE, routing_key="geonode.upload.copy_dynamic_model"), + Queue( + "geonode.upload.copy_geonode_data_table", GEONODE_EXCHANGE, routing_key="geonode.upload.copy_geonode_data_table" ), - Queue("importer.copy_dynamic_model", GEONODE_EXCHANGE, routing_key="importer.copy_dynamic_model"), - Queue("importer.copy_geonode_data_table", GEONODE_EXCHANGE, routing_key="importer.copy_geonode_data_table"), - Queue("importer.copy_raster_file", GEONODE_EXCHANGE, routing_key="importer.copy_raster_file"), - Queue("importer.rollback", GEONODE_EXCHANGE, routing_key="importer.rollback"), + Queue("geonode.upload.copy_raster_file", GEONODE_EXCHANGE, routing_key="geonode.upload.copy_raster_file"), + Queue("geonode.upload.rollback", GEONODE_EXCHANGE, routing_key="geonode.upload.rollback"), ) -DATABASE_ROUTERS = ["importer.db_router.DatastoreRouter"] - -SIZE_RESTRICTED_FILE_UPLOAD_ELEGIBLE_URL_NAMES += ("importer_upload",) +DATABASE_ROUTERS = ["geonode.upload.db_router.DatastoreRouter"] IMPORTER_HANDLERS = ast.literal_eval(os.getenv("IMPORTER_HANDLERS", "[]")) diff --git a/geonode/social/signals.py b/geonode/social/signals.py index fd759b2d826..06335780025 100644 --- a/geonode/social/signals.py +++ b/geonode/social/signals.py @@ -32,18 +32,13 @@ from geonode.layers.models import Dataset from geonode.maps.models import Map from geonode.documents.models import Document -from geonode.notifications_helper import ( - send_notification, - queue_notification, - get_notification_recipients, -) + logger = logging.getLogger(__name__) activity = None if "actstream" in settings.INSTALLED_APPS: from actstream import action as activity - from actstream.actions import follow, unfollow def activity_post_modify_object(sender, instance, created=None, **kwargs): @@ -143,18 +138,6 @@ def activity_post_modify_object(sender, instance, created=None, **kwargs): logger.warning("The activity received a non-actionable Model or None as the actor/action.") -def relationship_post_save_actstream(instance, sender, created, **kwargs): - follow(instance.from_user, instance.to_user) - - -def relationship_pre_delete_actstream(instance, sender, **kwargs): - unfollow(instance.from_user, instance.to_user) - - -def relationship_post_save(instance, sender, created, **kwargs): - queue_notification([instance.to_user], "user_follow", {"from_user": instance.from_user}) - - if activity: signals.post_save.connect(activity_post_modify_object, sender=Dataset) signals.post_delete.connect(activity_post_modify_object, sender=Dataset) @@ -166,14 +149,3 @@ def relationship_post_save(instance, sender, created, **kwargs): signals.post_delete.connect(activity_post_modify_object, sender=Document) signals.post_save.connect(activity_post_modify_object, sender=GeoApp) signals.post_delete.connect(activity_post_modify_object, sender=GeoApp) - - -def rating_post_save(instance, sender, created, **kwargs): - """Send a notification when rating a layer, map or document""" - notice_type_label = f"{instance.content_object.class_name.lower()}_rated" - recipients = get_notification_recipients(notice_type_label, instance.user, resource=instance.content_object) - send_notification( - recipients, - notice_type_label, - {"resource": instance.content_object, "user": instance.user, "rating": instance.rating}, - ) diff --git a/geonode/static/geonode/js/upload/LayerInfo.js b/geonode/static/geonode/js/upload/LayerInfo.js index cb6500cb0ab..3f6cb378909 100644 --- a/geonode/static/geonode/js/upload/LayerInfo.js +++ b/geonode/static/geonode/js/upload/LayerInfo.js @@ -263,6 +263,7 @@ define(function (require, exports) { } form_data.append('charset', $('#charset').val()); + form_data.append('source', "resource_file_upload"); if ($('#id_metadata_upload_form').prop('checked')) { form_data.append('metadata_upload_form', true); form_data.append('dataset_title', $('#id_dataset_title').val()); @@ -274,6 +275,7 @@ define(function (require, exports) { form_data.append('style_upload_form', true); form_data.append('dataset_title', $('#id_dataset_title').val()); } + console.log(form_data) return form_data; }; @@ -682,7 +684,9 @@ define(function (require, exports) { LayerInfo.prototype.uploadFiles = function (callback, array) { var form_data = this.prepareFormData(), self = this; var prog = ""; - + console.log("###########") + console.log(form_data) + console.log("###########") $.ajax({ url: form_target, async: true, diff --git a/geonode/tests/bdd/e2e/test_login.py b/geonode/tests/bdd/e2e/test_login.py deleted file mode 100644 index ddedb33d316..00000000000 --- a/geonode/tests/bdd/e2e/test_login.py +++ /dev/null @@ -1,90 +0,0 @@ -######################################################################### -# -# Copyright (C) 2018 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 . -# -######################################################################### - -"""User can login using authentication feature tests.""" - -import pytest -from django.contrib.auth import get_user_model -from pytest_bdd import given, scenario, then, when - - -# https://github.com/pytest-dev/pytest-django/issues/329 -@pytest.mark.django_db(transaction=True) # , serialized_rollback=True) -@scenario("features/login.feature", "User can access login page") -def test_user_can_access_login_page(db, geonode_db_setup): - """User can access login page.""" - # pass - - -@pytest.mark.django_db(transaction=True) # , serialized_rollback=True) -@scenario("features/login.feature", "Admin user") -def test_admin_user(): - """Admin user.""" - # pass - - -@given("I have an admin account") -def admin_user(): - """I have an admin account.""" - - -@given('A global administrator named "admin"') -def administrator_named_admin(): - """A global administrator named "admin".""" - admin = get_user_model().objects.filter(username="admin") - assert admin.exists() is True - - -@when('I go to the "login" page') -def go_to_login(en_browser): - """I go to the "login" page.""" - assert en_browser.is_text_present("Username") - assert en_browser.is_text_present("Password") - - -@when('I fill in "Username" with "admin"') -def fill_username(en_browser): - """I fill in "Username" with "admin".""" - assert en_browser.is_text_present("Username") - en_browser.fill("login", "admin") - - -@when('I fill in "Password" with "admin"') -def fill_password(en_browser): - """I fill in "Password" with "admin".""" - assert en_browser.is_text_present("Password") - en_browser.fill("password", "admin") - - -@when('I press the "Log in" button') -def login_user(en_browser): - """I press the "Log in" button.""" - en_browser.find_by_css("button[type=submit]").first.click() - - -@then('I see "Log in"') -def login_page(en_browser): - """I see "Log in".""" - assert en_browser.find_by_css("button[type=submit]") - - -@then('I should see "admin"') -def authenticated_page(en_browser): - """I should see "admin".""" - assert en_browser.find_by_xpath("//a[contains(@class, 'dropdown-toggle avatar')]") diff --git a/geonode/tests/smoke.py b/geonode/tests/smoke.py index 1317fd8cc36..abe0b1b89c4 100644 --- a/geonode/tests/smoke.py +++ b/geonode/tests/smoke.py @@ -186,15 +186,6 @@ def test_inverse_mercator(self): self.assertAlmostEqual(sw[0], -180.0, places=3, msg="SW lon is correct") self.assertAlmostEqual(sw[1], -90.0, places=3, msg="SW lat is correct") - def test_split_query(self): - query = 'alpha "beta gamma" delta ' - from geonode.utils import _split_query - - keywords = _split_query(query) - self.assertEqual(keywords[0], "alpha") - self.assertEqual(keywords[1], "beta gamma") - self.assertEqual(keywords[2], "delta") - class PermissionViewTests(GeoNodeBaseTestSupport): def setUp(self): diff --git a/geonode/tests/test_utils.py b/geonode/tests/test_utils.py index 15b7cf2ef88..e39600b60f2 100644 --- a/geonode/tests/test_utils.py +++ b/geonode/tests/test_utils.py @@ -16,15 +16,10 @@ # along with this program. If not, see . # ######################################################################### -import os import copy -import shutil from unittest import TestCase -import zipfile -import tempfile from django.test import override_settings -from osgeo import ogr from unittest.mock import patch from datetime import datetime, timedelta @@ -37,7 +32,7 @@ from geonode.geoserver.helpers import set_attributes from geonode.tests.base import GeoNodeBaseTestSupport from geonode.br.management.commands.utils.utils import ignore_time -from geonode.utils import copy_tree, fixup_shp_columnnames, get_supported_datasets_file_types, unzip_file, bbox_to_wkt +from geonode.utils import copy_tree, get_supported_datasets_file_types, bbox_to_wkt from geonode import settings @@ -113,50 +108,6 @@ def test_backup_of_child_directories( self.assertTrue(patch_shutil_copytree.called) -class TestFixupShp(GeoNodeBaseTestSupport): - def test_fixup_shp_columnnames(self): - project_root = os.path.abspath(os.path.dirname(__file__)) - dataset_zip = os.path.join(project_root, "data", "ming_female_1.zip") - - self.failUnless(zipfile.is_zipfile(dataset_zip)) - - dataset_shp = unzip_file(dataset_zip) - - expected_fieldnames = [ - "ID", - "_f", - "__1", - "__2", - "m", - "_", - "_M2", - "_M2_1", - "l", - "x", - "y", - "_WU", - "_1", - ] - _, _, fieldnames = fixup_shp_columnnames(dataset_shp, "windows-1258") - - inDriver = ogr.GetDriverByName("ESRI Shapefile") - inDataSource = inDriver.Open(dataset_shp, 0) - inLayer = inDataSource.GetLayer() - inLayerDefn = inLayer.GetLayerDefn() - - self.assertEqual(inLayerDefn.GetFieldCount(), len(expected_fieldnames)) - - for i, fn in enumerate(expected_fieldnames): - self.assertEqual(inLayerDefn.GetFieldDefn(i).GetName(), fn) - - inDataSource.Destroy() - - # Cleanup temp dir - shp_parent = os.path.dirname(dataset_shp) - if shp_parent.startswith(tempfile.gettempdir()): - shutil.rmtree(shp_parent, ignore_errors=True) - - class TestSetAttributes(GeoNodeBaseTestSupport): def setUp(self): super().setUp() diff --git a/geonode/tests/utils.py b/geonode/tests/utils.py index b861f2a0e8a..3a51535400f 100644 --- a/geonode/tests/utils.py +++ b/geonode/tests/utils.py @@ -23,21 +23,12 @@ import base64 import pickle import requests -from urllib.parse import urlencode, urlsplit -from urllib.request import ( - urljoin, - urlopen, - build_opener, - install_opener, - HTTPCookieProcessor, - HTTPPasswordMgrWithDefaultRealm, - HTTPBasicAuthHandler, -) -from urllib.error import HTTPError, URLError +from urllib.parse import urlsplit +from urllib.request import urljoin +from urllib.error import HTTPError from urllib3.exceptions import ProtocolError from requests.exceptions import ConnectionError import logging -import contextlib from io import IOBase from bs4 import BeautifulSoup @@ -241,60 +232,6 @@ def get_csrf_token(self, last=False): return self._session.cookies.get("csrftoken") -def get_web_page(url, username=None, password=None, login_url=None): - """Get url page possible with username and password.""" - - if login_url: - # Login via a form - cookies = HTTPCookieProcessor() - opener = build_opener(cookies) - install_opener(opener) - - opener.open(login_url) - - try: - token = [x.value for x in cookies.cookiejar if x.name == "csrftoken"][0] - except IndexError: - return False, "no csrftoken" - - params = dict( - username=username, - password=password, - this_is_the_login_form=True, - csrfmiddlewaretoken=token, - ) - encoded_params = urlencode(params) - - with contextlib.closing(opener.open(login_url, encoded_params)) as f: - f.read() - elif username is not None: - # Login using basic auth - - # Create password manager - passman = HTTPPasswordMgrWithDefaultRealm() - passman.add_password(None, url, username, password) - - # create the handler - authhandler = HTTPBasicAuthHandler(passman) - opener = build_opener(authhandler) - install_opener(opener) - - try: - pagehandle = urlopen(url) - except HTTPError as e: - msg = f"The server couldn't fulfill the request. Error code: {e.status_code}" - e.args = (msg,) - raise - except URLError as e: - msg = f'Could not open URL "{url}": {e}' - e.args = (msg,) - raise - else: - page = pagehandle.read() - - return page - - def check_dataset(uploaded): """Verify if an object is a valid Dataset.""" msg = f"Was expecting dataset object, got {type(uploaded)}" diff --git a/geonode/thumbs/tests/test_integration.py b/geonode/thumbs/tests/test_integration.py index c1d2dcda1bc..14eb06f96a9 100644 --- a/geonode/thumbs/tests/test_integration.py +++ b/geonode/thumbs/tests/test_integration.py @@ -697,11 +697,9 @@ def test_UTM_dataset_thumbnail(self): res = None try: dt_files = [os.path.join(os.path.abspath(os.path.dirname(__file__)), "data", "WY_USNG.zip")] - defaults = {"owner": self.user_admin} # raises an exception if resource_type is not provided - self.rm.ingest(dt_files) + res = self.rm.create(None, resource_type=Dataset, defaults={"owner": self.user_admin, "files": dt_files}) # ingest with datasets - res = self.rm.ingest(dt_files, resource_type=Dataset, defaults=defaults) if ( res ): # Since importing this dataset takes some time, the connection might be reset due to very low timeout set for testing. diff --git a/geonode/thumbs/utils.py b/geonode/thumbs/utils.py index fa360e3f50b..ab61eb982d4 100644 --- a/geonode/thumbs/utils.py +++ b/geonode/thumbs/utils.py @@ -445,11 +445,6 @@ def thumb_size(filepath): return 0 -def thumb_open(filename): - """Returns file handler of a thumbnail on the storage""" - return storage_manager.open(thumb_path(filename)) - - def get_thumbs(): """Fetches a list of all stored thumbnails""" if not storage_manager.exists(settings.THUMBNAIL_LOCATION): diff --git a/geonode/upload/__init__.py b/geonode/upload/__init__.py index 37b01855105..4138c65cab6 100644 --- a/geonode/upload/__init__.py +++ b/geonode/upload/__init__.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2022 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 @@ -16,7 +16,13 @@ # along with this program. If not, see . # ######################################################################### +import os +project_dir = os.path.dirname(os.path.abspath(__file__)) -class LayerNotReady(Exception): - pass +VERSION = (1, 1, 0) +__version__ = ".".join([str(i) for i in VERSION]) +__author__ = "geosolutions-it" +__email__ = "info@geosolutionsgroup.com" +__url__ = "https://github.com/GeoNode/geonode-importer" +default_app_config = "importer.apps.ImporterConfig" diff --git a/geonode/upload/admin.py b/geonode/upload/admin.py index b7e945bf95d..faa9961df7d 100644 --- a/geonode/upload/admin.py +++ b/geonode/upload/admin.py @@ -18,7 +18,6 @@ ######################################################################### from geonode.upload.models import ( - Upload, UploadParallelismLimit, UploadSizeLimit, ) @@ -75,6 +74,5 @@ def has_add_permission(self, request): return False -admin.site.register(Upload, UploadAdmin) admin.site.register(UploadSizeLimit, UploadSizeLimitAdmin) admin.site.register(UploadParallelismLimit, UploadParallelismLimitAdmin) diff --git a/geonode/upload/api/__init__.py b/geonode/upload/api/__init__.py index 6b4db6084e8..50b39831df7 100644 --- a/geonode/upload/api/__init__.py +++ b/geonode/upload/api/__init__.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2021 OSGeo +# Copyright (C) 2024 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 diff --git a/geonode/upload/api/exceptions.py b/geonode/upload/api/exceptions.py index 59c9a861531..50845ddb117 100644 --- a/geonode/upload/api/exceptions.py +++ b/geonode/upload/api/exceptions.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2022 OSGeo +# Copyright (C) 2024 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 @@ -40,3 +40,52 @@ class UploadParallelismLimitException(APIException): default_detail = _("The number of active parallel uploads exceeds. Wait for the pending ones to finish.") default_code = "upload_parallelism_limit_exceeded" category = "upload" + + +class ImportException(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "Exception during resource upload" + default_code = "importer_exception" + category = "importer" + + +class InvalidInputFileException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The provided files are invalid" + default_code = "importer_exception" + category = "importer" + + +class PublishResourceException(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "Error during the resource publishing" + default_code = "publishing_exception" + category = "importer" + + +class ResourceCreationException(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "Error during the creation of the geonode resource" + default_code = "gn_resource_exception" + category = "importer" + + +class CopyResourceException(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "Error during the copy of the geonode resource" + default_code = "gn_resource_exception" + category = "importer" + + +class StartImportException(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "Error during start of the import session" + default_code = "start_import_exception" + category = "importer" + + +class HandlerException(APIException): + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "base handler exception" + default_code = "handler_exception" + category = "handler" diff --git a/geonode/upload/api/permissions.py b/geonode/upload/api/permissions.py index d61e10c7d1c..31cbe84837d 100644 --- a/geonode/upload/api/permissions.py +++ b/geonode/upload/api/permissions.py @@ -16,7 +16,6 @@ # along with this program. If not, see . # ######################################################################### -from geonode.upload.models import Upload from rest_framework.filters import BaseFilterBackend @@ -34,7 +33,7 @@ def filter_queryset(self, request, queryset, view): user = request.user if not user or user.is_anonymous or not user.is_authenticated: - return Upload.objects.none() + return None elif user.is_superuser: return queryset return queryset.filter(user=user) diff --git a/geonode/upload/api/serializer.py b/geonode/upload/api/serializer.py new file mode 100644 index 00000000000..4657ddb9f3d --- /dev/null +++ b/geonode/upload/api/serializer.py @@ -0,0 +1,85 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework import serializers +from dynamic_rest.serializers import DynamicModelSerializer +from geonode.base.api.serializers import BaseDynamicModelSerializer +from geonode.base.models import ResourceBase +from geonode.upload.models import UploadParallelismLimit, UploadSizeLimit + + +class ImporterSerializer(DynamicModelSerializer): + class Meta: + ref_name = "ImporterSerializer" + model = ResourceBase + view_name = "importer_upload" + fields = ( + "base_file", + "xml_file", + "sld_file", + "store_spatial_files", + "skip_existing_layers", + "source", + ) + + base_file = serializers.FileField() + xml_file = serializers.FileField(required=False) + sld_file = serializers.FileField(required=False) + store_spatial_files = serializers.BooleanField(required=False, default=True) + skip_existing_layers = serializers.BooleanField(required=False, default=False) + source = serializers.CharField(required=False, default="upload") + + +class OverwriteImporterSerializer(ImporterSerializer): + class Meta: + ref_name = "OverwriteImporterSerializer" + model = ResourceBase + view_name = "importer_upload" + fields = ImporterSerializer.Meta.fields + ( + "overwrite_existing_layer", + "resource_pk", + ) + + overwrite_existing_layer = serializers.BooleanField(required=True) + resource_pk = serializers.IntegerField(required=True) + + +class UploadSizeLimitSerializer(BaseDynamicModelSerializer): + class Meta: + model = UploadSizeLimit + name = "upload-size-limit" + view_name = "upload-size-limits-list" + fields = ( + "slug", + "description", + "max_size", + "max_size_label", + ) + + +class UploadParallelismLimitSerializer(BaseDynamicModelSerializer): + class Meta: + model = UploadParallelismLimit + name = "upload-parallelism-limit" + view_name = "upload-parallelism-limits-list" + fields = ( + "slug", + "description", + "max_number", + ) + read_only_fields = ("slug",) diff --git a/geonode/upload/api/serializers.py b/geonode/upload/api/serializers.py deleted file mode 100644 index 664cdbb8154..00000000000 --- a/geonode/upload/api/serializers.py +++ /dev/null @@ -1,254 +0,0 @@ -######################################################################### -# -# Copyright (C) 2021 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 os - -from rest_framework import serializers - -from dynamic_rest.fields.fields import ( - DynamicRelationField, - DynamicComputedField, -) - -from geonode.upload.models import ( - Upload, - UploadParallelismLimit, - UploadSizeLimit, -) -from geonode.base.models import ResourceBase -from geonode.utils import build_absolute_uri -from geonode.layers.api.serializers import DatasetSerializer -from geonode.base.api.serializers import BaseDynamicModelSerializer - -import logging - -logger = logging.getLogger(__name__) - - -class UploadFileField(serializers.RelatedField): - class Meta: - model = ResourceBase - name = "resource-files" - - def to_representation(self, obj): - files = [] - for file in obj.files: - name, _ = os.path.splitext(os.path.basename(file)) - files.append({"name": name, "slug": os.path.basename(file), "file": file}) - return { - "name": obj.title, - "files": files, - } - - -class SessionSerializer(serializers.Field): - @classmethod - def _decode_target(cls, obj): - if obj: - return { - "name": getattr(obj, "name", None), - "type": getattr(obj, "type", None), - "enabled": getattr(obj, "enabled", None), - } - return obj - - @classmethod - def _decode_data(cls, obj): - if obj: - return { - "type": getattr(obj, "type", None), - "format": getattr(obj, "format", None), - "charset": getattr(obj, "charset", None), - "charsetEncoding": getattr(obj, "charsetEncoding", None), - "location": getattr(obj, "location", None), - "file": getattr(obj, "file", None), - "files": getattr(obj, "files", None), - "username": getattr(obj, "username", None), - } - return obj - - @classmethod - def _decode_layer(cls, obj): - if obj: - return { - "name": getattr(obj, "name", None), - "href": getattr(obj, "href", None), - "originalName": getattr(obj, "originalName", None), - "nativeName": getattr(obj, "nativeName", None), - "srs": getattr(obj, "srs", None), - "attributes": SessionSerializer._decode_layer_attributes(getattr(obj, "attributes", None)), - "bbox": SessionSerializer._decode_layer_bbox(getattr(obj, "bbox", None)), - } - return obj - - @classmethod - def _decode_layer_attributes(cls, objs): - if objs: - _a = [] - for obj in objs: - _a.append({"name": getattr(obj, "name", None), "binding": getattr(obj, "binding", None)}) - return _a - return objs - - @classmethod - def _decode_layer_bbox(cls, obj): - if obj: - return { - "minx": getattr(obj, "minx", None), - "miny": getattr(obj, "miny", None), - "maxx": getattr(obj, "maxx", None), - "maxy": getattr(obj, "maxy", None), - "crs": getattr(obj, "crs", None), - } - return obj - - def to_internal_value(self, data): - return data - - def to_representation(self, value): - if value: - _s = { - "name": value.name, - "charset": value.charset, - "permissions": value.permissions, - "time_transforms": value.time_transforms, - "update_mode": value.update_mode, - "time": value.time, - "dataset_title": value.dataset_title, - "dataset_abstract": value.dataset_abstract, - "completed_step": value.completed_step, - "error_msg": value.error_msg, - "upload_type": value.upload_type, - "time_info": value.time_info, - "mosaic": value.mosaic, - "append_to_mosaic_opts": value.append_to_mosaic_opts, - "append_to_mosaic_name": value.append_to_mosaic_name, - "mosaic_time_regex": value.mosaic_time_regex, - "mosaic_time_value": value.mosaic_time_value, - } - if getattr(value, "import_session"): - _import_session = value.import_session - _s["import_session"] = { - "id": _import_session.id, - "href": _import_session.href, - "state": _import_session.state, - "archive": _import_session.archive, - "targetWorkspace": SessionSerializer._decode_target( - getattr(_import_session, "targetWorkspace", None) - ), - "targetStore": SessionSerializer._decode_target(getattr(_import_session, "targetStore", None)), - "tasks": [], - } - for _task in _import_session.tasks: - _s["import_session"]["tasks"].append( - { - "id": _task.id, - "href": _task.href, - "state": _task.state, - "progress": getattr(_task, "progress", None), - "updateMode": getattr(_task, "updateMode", None), - "data": SessionSerializer._decode_data(getattr(_task, "data", None)), - "target": SessionSerializer._decode_target(getattr(_task, "target", None)), - "layer": SessionSerializer._decode_layer(getattr(_task, "layer", None)), - } - ) - return _s - - -class ProgressField(DynamicComputedField): - def get_attribute(self, instance): - return instance.progress - - -class ProgressUrlField(DynamicComputedField): - def __init__(self, type, **kwargs): - self.type = type - super().__init__(**kwargs) - - def get_attribute(self, instance): - try: - func = getattr(instance, f"get_{self.type}_url") - return build_absolute_uri(func()) - except AttributeError as e: - logger.exception(e) - return None - - -class UploadSerializer(BaseDynamicModelSerializer): - def __init__(self, *args, **kwargs): - # Instantiate the superclass normally - super().__init__(*args, **kwargs) - - request = self.context.get("request", None) - if request and request.query_params.get("full"): - self.fields["resource"] = DynamicRelationField(DatasetSerializer, embed=True, many=False, read_only=True) - self.fields["session"] = SessionSerializer(source="get_session", read_only=True) - - class Meta: - model = Upload - name = "upload" - view_name = "uploads-list" - fields = ( - "id", - "name", - "date", - "create_date", - "user", - "state", - "progress", - "complete", - "import_id", - "resume_url", - "delete_url", - "import_url", - "detail_url", - "uploadfile_set", - ) - - progress = ProgressField(read_only=True) - resume_url = ProgressUrlField("resume", read_only=True) - delete_url = ProgressUrlField("delete", read_only=True) - import_url = ProgressUrlField("import", read_only=True) - detail_url = ProgressUrlField("detail", read_only=True) - uploadfile_set = UploadFileField(source="resource", read_only=True) - - -class UploadSizeLimitSerializer(BaseDynamicModelSerializer): - class Meta: - model = UploadSizeLimit - name = "upload-size-limit" - view_name = "upload-size-limits-list" - fields = ( - "slug", - "description", - "max_size", - "max_size_label", - ) - - -class UploadParallelismLimitSerializer(BaseDynamicModelSerializer): - class Meta: - model = UploadParallelismLimit - name = "upload-parallelism-limit" - view_name = "upload-parallelism-limits-list" - fields = ( - "slug", - "description", - "max_number", - ) - read_only_fields = ("slug",) diff --git a/geonode/upload/api/tests.py b/geonode/upload/api/tests.py index 75a4dee41fc..5d6aa237c6f 100644 --- a/geonode/upload/api/tests.py +++ b/geonode/upload/api/tests.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2021 OSGeo +# Copyright (C) 2024 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 @@ -16,735 +16,214 @@ # along with this program. If not, see . # ######################################################################### +from django.contrib.auth import get_user_model +from django.core.files.uploadedfile import SimpleUploadedFile +from geonode.layers.models import Dataset +from django.urls import reverse +from unittest.mock import MagicMock, patch -from geonode.base.models import ResourceBase -from geonode.resource.models import ExecutionRequest -from geonode.geoserver.helpers import gs_catalog -import os -import shutil -import logging -import tempfile -from io import IOBase -from unittest import mock -from gisdata import GOOD_DATA -from urllib.request import urljoin +# Create your tests here. +from geonode.upload import project_dir +from geonode.base.populate_test_data import create_single_dataset +from django.http import HttpResponse, QueryDict -from django.conf import settings +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload.tests.utils import ImporterBaseTestSupport +from geonode.upload.orchestrator import orchestrator +from django.utils.module_loading import import_string +from geonode.assets.models import LocalAsset -from django.urls import reverse -from django.contrib.auth import authenticate, get_user_model -from django.test.utils import override_settings -from requests_toolbelt.multipart.encoder import MultipartEncoder +class TestImporterViewSet(ImporterBaseTestSupport): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.url = reverse("importer_upload") -from rest_framework.test import APITestCase + def setUp(self): + self.dataset = create_single_dataset(name="test_dataset_copy") + self.copy_url = reverse("importer_resource_copy", args=[self.dataset.id]) -from seleniumrequests import Firefox + def tearDown(self): + Dataset.objects.filter(name="test_dataset_copy").delete() -# from selenium.common import exceptions -# from selenium.webdriver.common.by import By -from selenium.webdriver import FirefoxOptions -from selenium.webdriver.support.wait import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.firefox.firefox_binary import FirefoxBinary + def test_upload_method_not_allowed(self): + self.client.login(username="admin", password="admin") -from webdriver_manager.firefox import GeckoDriverManager + response = self.client.get(self.url) + self.assertEqual(405, response.status_code) -from geonode.tests.base import GeoNodeLiveTestSupport -from geonode.geoserver.helpers import ogc_server_settings -from geonode.upload.models import UploadSizeLimit, UploadParallelismLimit -from geonode.upload.tests.utils import GEONODE_USER, GEONODE_PASSWD, rest_upload_by_path + response = self.client.put(self.url) + self.assertEqual(405, response.status_code) -LIVE_SERVER_URL = "http://localhost:8001/" -GEOSERVER_URL = ogc_server_settings.LOCATION -GEOSERVER_USER, GEOSERVER_PASSWD = ogc_server_settings.credentials + response = self.client.patch(self.url) + self.assertEqual(405, response.status_code) -CURRENT_LOCATION = os.path.abspath(os.path.dirname(__file__)) + def test_raise_exception_if_file_is_not_a_handled(self): -logger = logging.getLogger(__name__) + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile(name="file.invalid", content=b"abc"), + } + response = self.client.post(self.url, data=payload) + self.assertEqual(500, response.status_code) + + def test_gpkg_raise_error_with_invalid_payload(self): + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile( + name="test.gpkg", + content=b'{"type": "FeatureCollection", "content": "some-content"}', + ), + "store_spatial_files": "invalid", + } + expected = { + "success": False, + "errors": ["Must be a valid boolean."], + "code": "invalid", + } + response = self.client.post(self.url, data=payload) -@override_settings( - DEBUG=True, - ALLOWED_HOSTS=["*"], - SITEURL=LIVE_SERVER_URL, - CSRF_COOKIE_SECURE=False, - CSRF_COOKIE_HTTPONLY=False, - CORS_ORIGIN_ALLOW_ALL=True, - SESSION_COOKIE_SECURE=False, - DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=5, -) -class UploadApiTests(GeoNodeLiveTestSupport, APITestCase): - port = 0 + self.assertEqual(400, response.status_code) + self.assertEqual(expected, response.json()) - @classmethod - def setUpClass(cls): - super().setUpClass() + @patch("geonode.upload.api.views.import_orchestrator") + def test_gpkg_task_is_called(self, patch_upload): + patch_upload.apply_async.side_effect = MagicMock() - try: - """Instantiate selenium driver instance""" - binary = FirefoxBinary("/usr/bin/firefox") - opts = FirefoxOptions() - opts.add_argument("--headless") - executable_path = GeckoDriverManager().install() - cls.selenium = Firefox(firefox_binary=binary, firefox_options=opts, executable_path=executable_path) - cls.selenium.implicitly_wait(10) - except Exception as e: - logger.error(e) + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile( + name="test.gpkg", + content=b'{"type": "FeatureCollection", "content": "some-content"}', + ), + "store_spatial_files": True, + } - @classmethod - def tearDownClass(cls): - """Quit selenium driver instance""" - try: - cls.selenium.quit() - except Exception as e: - logger.debug(e) - super().tearDownClass() + response = self.client.post(self.url, data=payload) - def setUp(self): - super().setUp() - self.temp_folder = tempfile.mkdtemp(dir=CURRENT_LOCATION) - self.session_id = None - self.csrf_token = None + self.assertEqual(201, response.status_code) - def tearDown(self): - shutil.rmtree(self.temp_folder, ignore_errors=True) - return super().tearDown() - - def set_session_cookies(self, url=None): - # selenium will set cookie domain based on current page domain - self.selenium.get(url or f"{self.live_server_url}/") - self.csrf_token = self.selenium.get_cookie("csrftoken")["value"] - self.session_id = self.selenium.get_cookie(settings.SESSION_COOKIE_NAME)["value"] - self.selenium.add_cookie( - {"name": settings.SESSION_COOKIE_NAME, "value": self.session_id, "secure": False, "path": "/"} - ) - self.selenium.add_cookie({"name": "csrftoken", "value": self.csrf_token, "secure": False, "path": "/"}) - - def click_button(self, label): - selector = f"//button[contains(., '{label}')]" - self.selenium.find_element_by_xpath(selector).click() - - def do_login(self, username=GEONODE_USER, password=GEONODE_PASSWD): - """Method to login the GeoNode site""" - assert authenticate(username=username, password=password) - self.assertTrue(self.client.login(username=username, password=password)) # Native django test client - - url = urljoin(settings.SITEURL, f"{reverse('account_login')}?next=/layers") - self.set_session_cookies(url) - self.selenium.save_screenshot(os.path.join(self.temp_folder, "login.png")) - - title = self.selenium.title - current_url = self.selenium.current_url - logger.debug(f" ---- title: {title} / current_url: {current_url}") - - username_input = self.selenium.find_element_by_xpath('//input[@id="id_login"][@type="text"]') - username_input.send_keys(username) - password_input = self.selenium.find_element_by_xpath('//input[@id="id_password"][@type="password"]') - password_input.send_keys(password) - - self.selenium.save_screenshot(os.path.join(self.temp_folder, "login-set_fields.png")) - self.click_button("Sign In") - self.selenium.save_screenshot(os.path.join(self.temp_folder, "login-sign_in.png")) - - title = self.selenium.title - current_url = self.selenium.current_url - logger.debug(f" ---- title: {title} / current_url: {current_url}") - - # Wait until the response is received - WebDriverWait(self.selenium, 10).until(EC.title_contains("Explore Layers")) - self.set_session_cookies(url) - - def do_logout(self): - url = urljoin(settings.SITEURL, f"{reverse('account_logout')}") - self.selenium.get(url) - self.click_button("Log out") - - def do_upload_step(self, step=None): - step = urljoin(settings.SITEURL, reverse("data_upload", args=[step] if step else [])) - return step - - def as_superuser(func): - def wrapper(self, *args, **kwargs): - self.do_login() - func(self, *args, **kwargs) - self.do_logout() - - return wrapper - - def live_upload_file(self, _file): - """function that uploads a file, or a collection of files, to - the GeoNode""" - spatial_files = ("dbf_file", "shx_file", "prj_file") - base, ext = os.path.splitext(_file) - params = { - # make public since wms client doesn't do authentication - "csrfmiddlewaretoken": self.csrf_token, - "permissions": '{ "users": {"AnonymousUser": ["view_resourcebase"]} , "groups":{}}', - "time": "false", - "charset": "UTF-8", - } - cookies = {settings.SESSION_COOKIE_NAME: self.session_id, "csrftoken": self.csrf_token} - headers = { - "X-CSRFToken": self.csrf_token, - "X-Requested-With": "XMLHttpRequest", - "Set-Cookie": f"csrftoken={self.csrf_token}; sessionid={self.session_id}", - } - url = self.do_upload_step() - logger.debug(f" ---- UPLOAD URL: {url} / cookies: {cookies} / headers: {headers}") - - # deal with shapefiles - if ext.lower() == ".shp": - for spatial_file in spatial_files: - ext, _ = spatial_file.split("_") - file_path = f"{base}.{ext}" - # sometimes a shapefile is missing an extra file, - # allow for that - if os.path.exists(file_path): - params[spatial_file] = open(file_path, "rb") - - with open(_file, "rb") as base_file: - params["base_file"] = base_file - for name, value in params.items(): - if isinstance(value, IOBase): - params[name] = (os.path.basename(value.name), value) - - # refresh to exchange cookies with the server. - self.selenium.refresh() - self.selenium.get(url) - self.selenium.save_screenshot(os.path.join(self.temp_folder, "upload-page.png")) - logger.debug(f" ------------ UPLOAD FORM: {params}") - encoder = MultipartEncoder(fields=params) - headers["Content-Type"] = encoder.content_type - response = self.selenium.request("POST", url, data=encoder, headers=headers) - - # Closes the files - for spatial_file in spatial_files: - if isinstance(params.get(spatial_file), IOBase): - params[spatial_file].close() - - try: - logger.error(f" -- response: {response.status_code} / {response.json()}") - return response, response.json() - except ValueError: - logger.exception(ValueError(f"probably not json, status {response.status_code} / {response.content}")) - return response, response.content - - @override_settings(CELERY_TASK_ALWAYS_EAGER=True) - @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o777) - @override_settings(FILE_UPLOAD_PERMISSIONS=0o777) - def test_rest_uploads(self): - """ - Ensure we can access the Local Server Uploads list. - """ - resp = None - layer_name = "relief_san_andres" - try: - self._cleanup_layer(layer_name=layer_name) - # Try to upload a good raster file and check the session IDs - fname = os.path.join(GOOD_DATA, "raster", "relief_san_andres.tif") - resp, data = rest_upload_by_path(fname, self.client) - self.assertEqual(resp.status_code, 201) - - url = reverse("uploads-list") - # Anonymous - self.client.logout() - response = self.client.get(url, format="json") - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 5) - self.assertEqual(response.data["total"], 0) - # Pagination - self.assertEqual(len(response.data["uploads"]), 0) - logger.debug(response.data) - except Exception: - json = resp.json() - if json.get("errors"): - logger.error(f"Error in upload: {json}") - try: - layer_name = json.get("errors")[0].split("for : ")[1].split(",")[0] - except IndexError as e: - logger.error(f"Could not parse layername from {json.get('errors')}", exc_info=e) - # TODO: make sure the _cleanup_layer will use the proper layer name - self.skipTest("Error with GeoServer") - finally: - self._cleanup_layer(layer_name) - - @override_settings(CELERY_TASK_ALWAYS_EAGER=True) - @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o777) - @override_settings(FILE_UPLOAD_PERMISSIONS=0o777) - def test_rest_uploads_non_interactive(self): - """ - Ensure we can access the Local Server Uploads list. - """ - resp = None - layer_name = "relief_san_andres" - try: - self._cleanup_layer(layer_name=layer_name) - # Try to upload a good raster file and check the session IDs - fname = os.path.join(GOOD_DATA, "raster", "relief_san_andres.tif") - resp, data = rest_upload_by_path(fname, self.client, non_interactive=True) - self.assertEqual(resp.status_code, 201) - exec_id = data.get("execution_id", None) - _exec = ExecutionRequest.objects.get(exec_id=exec_id) - self.assertEqual(_exec.status, "finished") - except Exception as e: - json = resp.json() - logger.warning(f"Error with GeoServer {json}: {e}", exc_info=e) - if json.get("errors"): - try: - layer_name = json.get("errors")[0].split("for : ")[1].split(",")[0] - except IndexError as e: - logger.error(f"Could not parse layername from {json.get('errors')}", exc_info=e) - # TODO: make sure the _cleanup_layer will use the proper layer name - self.skipTest("Error with GeoServer") - finally: - self._cleanup_layer(layer_name) - - def _cleanup_layer(self, layer_name): - # removing the layer from geonode - x = ResourceBase.objects.filter(alternate__icontains=layer_name) - if x.exists(): - for el in x.iterator(): - el.delete() - # removing the layer from geoserver - dataset = gs_catalog.get_layer(layer_name) - if dataset: - gs_catalog.delete(dataset, purge="all", recurse=True) - # removing the layer from geoserver - store = gs_catalog.get_store(layer_name, workspace="geonode") - if store: - gs_catalog.delete(store, purge="all", recurse=True) - - @mock.patch("geonode.upload.uploadhandler.SimpleUploadedFile") - def test_rest_uploads_with_size_limit(self, mocked_uploaded_file): - """ - Try to upload a file larger than allowed by ``dataset_upload_size`` - but not larger than ``file_upload_handler`` max_size. - """ - expected_error = { - "success": False, - "errors": ["Total upload size exceeds 1\xa0byte. Please try again with smaller files."], - "code": "importer_exception", + @patch("geonode.upload.api.views.import_orchestrator") + def test_geojson_task_is_called(self, patch_upload): + patch_upload.apply_async.side_effect = MagicMock() + + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile( + name="test.geojson", + content=b'{"type": "FeatureCollection", "content": "some-content"}', + ), + "store_spatial_files": True, } - upload_size_limit_obj, created = UploadSizeLimit.objects.get_or_create( - slug="dataset_upload_size", - defaults={ - "description": "The sum of sizes for the files of a dataset upload.", - "max_size": 1, - }, - ) - upload_size_limit_obj.max_size = 1 - upload_size_limit_obj.save() - # Try to upload and verify if it passed only by the form size validation - fname = os.path.join(GOOD_DATA, "raster", "relief_san_andres.tif") + response = self.client.post(self.url, data=payload) - max_size_path = "geonode.upload.uploadhandler.SizeRestrictedFileUploadHandler._get_max_size" - with mock.patch(max_size_path, new_callable=mock.PropertyMock) as max_size_mock: - max_size_mock.return_value = lambda x: 209715200 + self.assertEqual(201, response.status_code) - resp, data = rest_upload_by_path(fname, self.client) - self.assertEqual(resp.status_code, 500) - self.assertDictEqual(expected_error, data) - mocked_uploaded_file.assert_not_called() + self.assertTrue(201, response.status_code) + @patch("geonode.upload.api.views.import_orchestrator") + def test_zip_file_is_unzip_and_the_handler_is_found(self, patch_upload): + patch_upload.apply_async.side_effect = MagicMock() -class UploadSizeLimitTests(APITestCase): - fixtures = [ - "group_test_data.json", - ] + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": open(f"{project_dir}/tests/fixture/valid.zip", "rb"), + "zip_file": open(f"{project_dir}/tests/fixture/valid.zip", "rb"), + "store_spatial_files": True, + } - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.admin = get_user_model().objects.get(username="admin") - UploadSizeLimit.objects.create( - slug="some-size-limit", - description="some description", - max_size=104857600, # 100 MB - ) - UploadSizeLimit.objects.create( - slug="some-other-size-limit", - description="some other description", - max_size=52428800, # 50 MB - ) + response = self.client.post(self.url, data=payload) - def test_list_size_limits_admin_user(self): - url = reverse("upload-size-limits-list") - - # List as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.get(url) - - # Assertions - self.assertEqual(response.status_code, 200) - self.assertEqual(response.wsgi_request.user, self.admin) - # Response Content - size_limits = [ - (size_limit["slug"], size_limit["max_size"], size_limit["max_size_label"]) - for size_limit in response.json()["upload-size-limits"] - ] - expected_size_limits = [ - ("some-size-limit", 104857600, "100.0\xa0MB"), - ("some-other-size-limit", 52428800, "50.0\xa0MB"), - ] - for size_limit in expected_size_limits: - self.assertIn(size_limit, size_limits) - - def test_list_size_limits_anonymous_user(self): - url = reverse("upload-size-limits-list") - - # List as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.get(url) - - # Assertions - self.assertEqual(response.status_code, 200) - self.assertTrue(response.wsgi_request.user.is_anonymous) - # Response Content - size_limits = [ - (size_limit["slug"], size_limit["max_size"], size_limit["max_size_label"]) - for size_limit in response.json()["upload-size-limits"] - ] - expected_size_limits = [ - ("some-size-limit", 104857600, "100.0\xa0MB"), - ("some-other-size-limit", 52428800, "50.0\xa0MB"), - ] - for size_limit in expected_size_limits: - self.assertIn(size_limit, size_limits) - - def test_retrieve_size_limit_admin_user(self): - url = reverse("upload-size-limits-detail", args=("some-size-limit",)) - - # List as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.get(url) - - # Assertions - self.assertEqual(response.status_code, 200) - self.assertEqual(response.wsgi_request.user, self.admin) - # Response Content - size_limit = response.json()["upload-size-limit"] - self.assertEqual(size_limit["slug"], "some-size-limit") - self.assertEqual(size_limit["max_size"], 104857600) - self.assertEqual(size_limit["max_size_label"], "100.0\xa0MB") - - def test_retrieve_size_limit_anonymous_user(self): - url = reverse("upload-size-limits-detail", args=("some-size-limit",)) - - # List as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.get(url) - - # Assertions - self.assertEqual(response.status_code, 200) - self.assertTrue(response.wsgi_request.user.is_anonymous) - # Response Content - size_limit = response.json()["upload-size-limit"] - self.assertEqual(size_limit["slug"], "some-size-limit") - self.assertEqual(size_limit["max_size"], 104857600) - self.assertEqual(size_limit["max_size_label"], "100.0\xa0MB") - - def test_patch_size_limit_admin_user(self): - url = reverse("upload-size-limits-detail", args=("some-size-limit",)) - - # List as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.patch(url, data={"max_size": 5242880}) - - # Assertions - self.assertEqual(response.status_code, 405) - self.assertEqual(response.wsgi_request.user, self.admin) - - def test_patch_size_limit_anonymous_user(self): - url = reverse("upload-size-limits-detail", args=("some-size-limit",)) - - # List as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.patch(url, data={"max_size": 2621440}) - - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) - - def test_put_size_limit_admin_user(self): - url = reverse("upload-size-limits-detail", args=("some-size-limit",)) - - # List as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.put(url, data={"slug": "some-size-limit", "max_size": 5242880}) - - # Assertions - self.assertEqual(response.status_code, 405) - self.assertEqual(response.wsgi_request.user, self.admin) - - def test_put_size_limit_anonymous_user(self): - url = reverse("upload-size-limits-detail", args=("some-size-limit",)) - - # List as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.put(url, data={"slug": "some-size-limit", "max_size": 2621440}) - - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) - - def test_post_size_limit_admin_user(self): - url = reverse("upload-size-limits-list") - - # List as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.post(url, data={"slug": "some-new-slug", "max_size": 5242880}) - - # Assertions - self.assertEqual(response.status_code, 201) - self.assertEqual(response.wsgi_request.user, self.admin) - # Response Content - size_limit = response.json()["upload-size-limit"] - self.assertEqual(size_limit["slug"], "some-new-slug") - self.assertEqual(size_limit["max_size"], 5242880) - self.assertEqual(size_limit["max_size_label"], "5.0\xa0MB") - - def test_post_size_limit_anonymous_user(self): - url = reverse("upload-size-limits-list") - - # List as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.post(url, data={"slug": "other-new-slug", "max_size": 2621440}) + self.assertEqual(201, response.status_code) - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) + def test_copy_method_not_allowed(self): + self.client.force_login(get_user_model().objects.get(username="admin")) - def test_delete_size_limit_admin_user(self): - url = reverse("upload-size-limits-detail", args=("some-size-limit",)) + response = self.client.get(self.copy_url) + self.assertEqual(405, response.status_code) - # List as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.delete(url) + response = self.client.post(self.copy_url) + self.assertEqual(405, response.status_code) - # Assertions - self.assertEqual(response.status_code, 405) - self.assertEqual(response.wsgi_request.user, self.admin) + response = self.client.patch(self.copy_url) + self.assertEqual(405, response.status_code) - def test_delete_size_limit_anonymous_user(self): - url = reverse("upload-size-limits-detail", args=("some-other-size-limit",)) + @patch("geonode.upload.api.views.import_orchestrator") + @patch("geonode.upload.api.views.ResourceBaseViewSet.resource_service_copy") + def test_redirect_to_old_upload_if_file_handler_is_not_set(self, copy_view, _orc): + copy_view.return_value = HttpResponse() + self.client.force_login(get_user_model().objects.get(username="admin")) - # List as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.delete(url) + response = self.client.put(self.copy_url) - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) + self.assertEqual(200, response.status_code) + _orc.assert_not_called() + copy_view.assert_called_once() + @patch("geonode.upload.api.views.import_orchestrator") + def test_copy_ther_resource_if_file_handler_is_set(self, _orc): + user = get_user_model().objects.get(username="admin") + user.is_superuser = True + user.save() + self.client.force_login(get_user_model().objects.get(username="admin")) + ResourceHandlerInfo.objects.create( + resource=self.dataset, + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + payload = QueryDict("", mutable=True) + payload.update({"defaults": '{"title":"stili_di_vita_4scenari"}'}) + response = self.client.put(self.copy_url, data=payload, content_type="application/json") + + self.assertEqual(200, response.status_code) + _orc.s.assert_called_once() + + @patch("geonode.upload.api.views.import_orchestrator") + def test_asset_is_created_before_the_import_start(self, patch_upload): + patch_upload.apply_async.side_effect = MagicMock() + + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile( + name="test.geojson", + content=b'{"type": "FeatureCollection", "content": "some-content"}', + ), + "store_spatial_files": True, + } -class UploadParallelismLimitTests(APITestCase): - fixtures = [ - "group_test_data.json", - ] + response = self.client.post(self.url, data=payload) - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.admin = get_user_model().objects.get(username="admin") - cls.norman_user = get_user_model().objects.get(username="norman") - cls.test_user = get_user_model().objects.get(username="test_user") - try: - cls.default_parallelism_limit = UploadParallelismLimit.objects.get(slug="default_max_parallel_uploads") - except UploadParallelismLimit.DoesNotExist: - cls.default_parallelism_limit = UploadParallelismLimit.objects.create_default_limit() - - def test_list_parallelism_limits_admin_user(self): - url = reverse("upload-parallelism-limits-list") - - # List as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.get(url) - - # Assertions - self.assertEqual(response.status_code, 200) - self.assertEqual(response.wsgi_request.user, self.admin) - # Response Content - parallelism_limits = [ - (parallelism_limit["slug"], parallelism_limit["max_number"]) - for parallelism_limit in response.json()["upload-parallelism-limits"] - ] - expected_parallelism_limits = [ - (self.default_parallelism_limit.slug, self.default_parallelism_limit.max_number), - ] - for parallelism_limit in expected_parallelism_limits: - self.assertIn(parallelism_limit, parallelism_limits) - - def test_list_parallelism_limits_anonymous_user(self): - url = reverse("upload-parallelism-limits-list") - - # List as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.get(url) - - # Assertions - self.assertEqual(response.status_code, 200) - self.assertTrue(response.wsgi_request.user.is_anonymous) - # Response Content - parallelism_limits = [ - (parallelism_limit["slug"], parallelism_limit["max_number"]) - for parallelism_limit in response.json()["upload-parallelism-limits"] - ] - expected_parallelism_limits = [ - (self.default_parallelism_limit.slug, self.default_parallelism_limit.max_number), - ] - for parallelism_limit in expected_parallelism_limits: - self.assertIn(parallelism_limit, parallelism_limits) - - def test_retrieve_parallelism_limit_admin_user(self): - url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) - - # Retrieve as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.get(url) - - # Assertions - self.assertEqual(response.status_code, 200) - self.assertEqual(response.wsgi_request.user, self.admin) - # Response Content - parallelism_limit = response.json()["upload-parallelism-limit"] - self.assertEqual(parallelism_limit["slug"], self.default_parallelism_limit.slug) - self.assertEqual(parallelism_limit["max_number"], self.default_parallelism_limit.max_number) - - def test_retrieve_parallelism_limit_norman_user(self): - url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) - - # Retrieve as a norman user - self.client.force_authenticate(user=self.norman_user) - response = self.client.get(url) - - # Assertions - self.assertEqual(response.status_code, 200) - self.assertEqual(response.wsgi_request.user, self.norman_user) - # Response Content - parallelism_limit = response.json()["upload-parallelism-limit"] - self.assertEqual(parallelism_limit["slug"], self.default_parallelism_limit.slug) - self.assertEqual(parallelism_limit["max_number"], self.default_parallelism_limit.max_number) - - def test_patch_parallelism_limit_admin_user(self): - url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) - - # Patch as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.patch(url, data={"max_number": 3}) - - # Assertions - self.assertEqual(response.status_code, 405) - self.assertEqual(response.wsgi_request.user, self.admin) - - def test_patch_parallelism_limit_norman_user(self): - url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) - - # Patch as a norman user - self.client.force_authenticate(user=None) - response = self.client.patch(url, data={"max_number": 4}) - - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) - - def test_patch_parallelism_limit_anonymous_user(self): - url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) - - # Patch as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.patch(url, data={"max_number": 6}) - - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) - - def test_put_parallelism_limit_admin_user(self): - url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) - - # Put as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.put(url, data={"slug": self.default_parallelism_limit.slug, "max_number": 7}) - - # Assertions - self.assertEqual(response.status_code, 405) - self.assertEqual(response.wsgi_request.user, self.admin) - - def test_put_parallelism_limit_anonymous_user(self): - url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) - - # Put as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.put(url, data={"slug": self.default_parallelism_limit.slug, "max_number": 8}) - - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) - - def test_post_parallelism_limit_admin_user(self): - url = reverse("upload-parallelism-limits-list") - - # Post as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.post(url, data={"slug": "some-parallelism-limit", "max_number": 9}) - - # Assertions - self.assertEqual(response.status_code, 201) - self.assertEqual(response.wsgi_request.user, self.admin) - # Response Content - parallelism_limit = response.json()["upload-parallelism-limit"] - self.assertEqual(parallelism_limit["slug"], "some-parallelism-limit") - self.assertEqual(parallelism_limit["max_number"], 9) - - def test_post_parallelism_limit_anonymous_user(self): - url = reverse("upload-parallelism-limits-list") - - # Post as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.post(url, data={"slug": "some-parallelism-limit", "max_number": 8}) - - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) - - def test_delete_parallelism_limit_admin_user(self): - UploadParallelismLimit.objects.create( - slug="test-parallelism-limit", - max_number=123, - ) - url = reverse("upload-parallelism-limits-detail", args=("test-parallelism-limit",)) + self.assertEqual(201, response.status_code) - # Delete as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.delete(url) + self.assertTrue(201, response.status_code) - # Assertions - self.assertEqual(response.status_code, 405) - self.assertEqual(response.wsgi_request.user, self.admin) + _exec = orchestrator.get_execution_object(response.json()["execution_id"]) - def test_delete_parallelism_limit_admin_user_protected(self): - url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + asset_handler = import_string(_exec.input_params["asset_module_path"]) + self.assertTrue(asset_handler.objects.filter(id=_exec.input_params["asset_id"])) - # Delete as an admin user - self.client.force_authenticate(user=self.admin) - response = self.client.delete(url) + asset_handler.objects.filter(id=_exec.input_params["asset_id"]).delete() - # Assertions - self.assertEqual(response.status_code, 405) - self.assertEqual(response.wsgi_request.user, self.admin) + @patch("geonode.upload.api.views.import_orchestrator") + @patch("geonode.upload.api.views.UploadLimitValidator.validate_parallelism_limit_per_user") + def test_asset_should_be_deleted_if_created_during_with_exception( + self, validate_parallelism_limit_per_user, patch_upload + ): + patch_upload.apply_async.s.side_effect = MagicMock() + validate_parallelism_limit_per_user.side_effect = Exception("random exception") - def test_delete_parallelism_limit_anonymous_user(self): - UploadParallelismLimit.objects.create( - slug="test-parallelism-limit", - max_number=123, - ) - url = reverse("upload-parallelism-limits-detail", args=("test-parallelism-limit",)) + self.client.force_login(get_user_model().objects.get(username="admin")) + payload = { + "base_file": SimpleUploadedFile( + name="test.geojson", + content=b'{"type": "FeatureCollection", "content": "some-content"}', + ), + "store_spatial_files": True, + } - # Delete as an Anonymous user - self.client.force_authenticate(user=None) - response = self.client.delete(url) + response = self.client.post(self.url, data=payload) - # Assertions - self.assertEqual(response.status_code, 403) - self.assertTrue(response.wsgi_request.user.is_anonymous) + self.assertEqual(500, response.status_code) + self.assertFalse(LocalAsset.objects.exists()) diff --git a/geonode/upload/api/tests_old.py b/geonode/upload/api/tests_old.py new file mode 100644 index 00000000000..0cafbdbbab4 --- /dev/null +++ b/geonode/upload/api/tests_old.py @@ -0,0 +1,635 @@ +######################################################################### +# +# Copyright (C) 2021 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 geonode.base.models import ResourceBase +from geonode.geoserver.helpers import gs_catalog +import os +import shutil +import logging +import tempfile +from io import IOBase +from urllib.request import urljoin + +from django.conf import settings + +from django.urls import reverse +from django.contrib.auth import authenticate, get_user_model +from django.test.utils import override_settings + +from requests_toolbelt.multipart.encoder import MultipartEncoder + +from rest_framework.test import APITestCase + +from seleniumrequests import Firefox + +# from selenium.common import exceptions +# from selenium.webdriver.common.by import By +from selenium.webdriver import FirefoxOptions +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary + +from webdriver_manager.firefox import GeckoDriverManager + +from geonode.tests.base import GeoNodeLiveTestSupport +from geonode.geoserver.helpers import ogc_server_settings +from geonode.upload.models import UploadSizeLimit, UploadParallelismLimit + +LIVE_SERVER_URL = "http://localhost:8001/" +GEOSERVER_URL = ogc_server_settings.LOCATION +GEOSERVER_USER, GEOSERVER_PASSWD = ogc_server_settings.credentials + +CURRENT_LOCATION = os.path.abspath(os.path.dirname(__file__)) + +logger = logging.getLogger("importer") + + +@override_settings( + DEBUG=True, + ALLOWED_HOSTS=["*"], + SITEURL=LIVE_SERVER_URL, + CSRF_COOKIE_SECURE=False, + CSRF_COOKIE_HTTPONLY=False, + CORS_ORIGIN_ALLOW_ALL=True, + SESSION_COOKIE_SECURE=False, + DEFAULT_MAX_PARALLEL_UPLOADS_PER_USER=5, +) +class UploadApiTests(GeoNodeLiveTestSupport, APITestCase): + port = 0 + + @classmethod + def setUpClass(cls): + super().setUpClass() + + try: + """Instantiate selenium driver instance""" + binary = FirefoxBinary("/usr/bin/firefox") + opts = FirefoxOptions() + opts.add_argument("--headless") + executable_path = GeckoDriverManager().install() + cls.selenium = Firefox(firefox_binary=binary, firefox_options=opts, executable_path=executable_path) + cls.selenium.implicitly_wait(10) + except Exception as e: + logger.error(e) + + @classmethod + def tearDownClass(cls): + """Quit selenium driver instance""" + try: + cls.selenium.quit() + except Exception as e: + logger.debug(e) + super().tearDownClass() + + def setUp(self): + super().setUp() + self.temp_folder = tempfile.mkdtemp(dir=CURRENT_LOCATION) + self.session_id = None + self.csrf_token = None + + def tearDown(self): + shutil.rmtree(self.temp_folder, ignore_errors=True) + return super().tearDown() + + def set_session_cookies(self, url=None): + # selenium will set cookie domain based on current page domain + self.selenium.get(url or f"{self.live_server_url}/") + self.csrf_token = self.selenium.get_cookie("csrftoken")["value"] + self.session_id = self.selenium.get_cookie(settings.SESSION_COOKIE_NAME)["value"] + self.selenium.add_cookie( + {"name": settings.SESSION_COOKIE_NAME, "value": self.session_id, "secure": False, "path": "/"} + ) + self.selenium.add_cookie({"name": "csrftoken", "value": self.csrf_token, "secure": False, "path": "/"}) + + def click_button(self, label): + selector = f"//button[contains(., '{label}')]" + self.selenium.find_element_by_xpath(selector).click() + + def do_login(self, username="admin", password="admin"): + """Method to login the GeoNode site""" + assert authenticate(username=username, password=password) + self.assertTrue(self.client.login(username=username, password=password)) # Native django test client + + url = urljoin(settings.SITEURL, f"{reverse('account_login')}?next=/layers") + self.set_session_cookies(url) + self.selenium.save_screenshot(os.path.join(self.temp_folder, "login.png")) + + title = self.selenium.title + current_url = self.selenium.current_url + logger.debug(f" ---- title: {title} / current_url: {current_url}") + + username_input = self.selenium.find_element_by_xpath('//input[@id="id_login"][@type="text"]') + username_input.send_keys(username) + password_input = self.selenium.find_element_by_xpath('//input[@id="id_password"][@type="password"]') + password_input.send_keys(password) + + self.selenium.save_screenshot(os.path.join(self.temp_folder, "login-set_fields.png")) + self.click_button("Sign In") + self.selenium.save_screenshot(os.path.join(self.temp_folder, "login-sign_in.png")) + + title = self.selenium.title + current_url = self.selenium.current_url + logger.debug(f" ---- title: {title} / current_url: {current_url}") + + # Wait until the response is received + WebDriverWait(self.selenium, 10).until(EC.title_contains("Explore Layers")) + self.set_session_cookies(url) + + def do_logout(self): + url = urljoin(settings.SITEURL, f"{reverse('account_logout')}") + self.selenium.get(url) + self.click_button("Log out") + + def do_upload_step(self, step=None): + step = urljoin(settings.SITEURL, reverse("data_upload", args=[step] if step else [])) + return step + + def live_upload_file(self, _file): + """function that uploads a file, or a collection of files, to + the GeoNode""" + spatial_files = ("dbf_file", "shx_file", "prj_file") + base, ext = os.path.splitext(_file) + params = { + # make public since wms client doesn't do authentication + "csrfmiddlewaretoken": self.csrf_token, + "permissions": '{ "users": {"AnonymousUser": ["view_resourcebase"]} , "groups":{}}', + "time": "false", + "charset": "UTF-8", + } + cookies = {settings.SESSION_COOKIE_NAME: self.session_id, "csrftoken": self.csrf_token} + headers = { + "X-CSRFToken": self.csrf_token, + "X-Requested-With": "XMLHttpRequest", + "Set-Cookie": f"csrftoken={self.csrf_token}; sessionid={self.session_id}", + } + url = self.do_upload_step() + logger.debug(f" ---- UPLOAD URL: {url} / cookies: {cookies} / headers: {headers}") + + # deal with shapefiles + if ext.lower() == ".shp": + for spatial_file in spatial_files: + ext, _ = spatial_file.split("_") + file_path = f"{base}.{ext}" + # sometimes a shapefile is missing an extra file, + # allow for that + if os.path.exists(file_path): + params[spatial_file] = open(file_path, "rb") + + with open(_file, "rb") as base_file: + params["base_file"] = base_file + for name, value in params.items(): + if isinstance(value, IOBase): + params[name] = (os.path.basename(value.name), value) + + # refresh to exchange cookies with the server. + self.selenium.refresh() + self.selenium.get(url) + self.selenium.save_screenshot(os.path.join(self.temp_folder, "upload-page.png")) + logger.debug(f" ------------ UPLOAD FORM: {params}") + encoder = MultipartEncoder(fields=params) + headers["Content-Type"] = encoder.content_type + response = self.selenium.request("POST", url, data=encoder, headers=headers) + + # Closes the files + for spatial_file in spatial_files: + if isinstance(params.get(spatial_file), IOBase): + params[spatial_file].close() + + try: + logger.error(f" -- response: {response.status_code} / {response.json()}") + return response, response.json() + except ValueError: + logger.exception(ValueError(f"probably not json, status {response.status_code} / {response.content}")) + return response, response.content + + def _cleanup_layer(self, layer_name): + # removing the layer from geonode + x = ResourceBase.objects.filter(alternate__icontains=layer_name) + if x.exists(): + for el in x.iterator(): + el.delete() + # removing the layer from geoserver + dataset = gs_catalog.get_layer(layer_name) + if dataset: + gs_catalog.delete(dataset, purge="all", recurse=True) + # removing the layer from geoserver + store = gs_catalog.get_store(layer_name, workspace="geonode") + if store: + gs_catalog.delete(store, purge="all", recurse=True) + + +class UploadSizeLimitTests(APITestCase): + fixtures = [ + "group_test_data.json", + ] + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.admin = get_user_model().objects.get(username="admin") + UploadSizeLimit.objects.create( + slug="some-size-limit", + description="some description", + max_size=104857600, # 100 MB + ) + UploadSizeLimit.objects.create( + slug="some-other-size-limit", + description="some other description", + max_size=52428800, # 50 MB + ) + + def test_list_size_limits_admin_user(self): + url = reverse("upload-size-limits-list") + + # List as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.get(url) + + # Assertions + self.assertEqual(response.status_code, 200) + self.assertEqual(response.wsgi_request.user, self.admin) + # Response Content + size_limits = [ + (size_limit["slug"], size_limit["max_size"], size_limit["max_size_label"]) + for size_limit in response.json()["upload-size-limits"] + ] + expected_size_limits = [ + ("some-size-limit", 104857600, "100.0\xa0MB"), + ("some-other-size-limit", 52428800, "50.0\xa0MB"), + ] + for size_limit in expected_size_limits: + self.assertIn(size_limit, size_limits) + + def test_list_size_limits_anonymous_user(self): + url = reverse("upload-size-limits-list") + + # List as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.get(url) + + # Assertions + self.assertEqual(response.status_code, 200) + self.assertTrue(response.wsgi_request.user.is_anonymous) + # Response Content + size_limits = [ + (size_limit["slug"], size_limit["max_size"], size_limit["max_size_label"]) + for size_limit in response.json()["upload-size-limits"] + ] + expected_size_limits = [ + ("some-size-limit", 104857600, "100.0\xa0MB"), + ("some-other-size-limit", 52428800, "50.0\xa0MB"), + ] + for size_limit in expected_size_limits: + self.assertIn(size_limit, size_limits) + + def test_retrieve_size_limit_admin_user(self): + url = reverse("upload-size-limits-detail", args=("some-size-limit",)) + + # List as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.get(url) + + # Assertions + self.assertEqual(response.status_code, 200) + self.assertEqual(response.wsgi_request.user, self.admin) + # Response Content + size_limit = response.json()["upload-size-limit"] + self.assertEqual(size_limit["slug"], "some-size-limit") + self.assertEqual(size_limit["max_size"], 104857600) + self.assertEqual(size_limit["max_size_label"], "100.0\xa0MB") + + def test_retrieve_size_limit_anonymous_user(self): + url = reverse("upload-size-limits-detail", args=("some-size-limit",)) + + # List as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.get(url) + + # Assertions + self.assertEqual(response.status_code, 200) + self.assertTrue(response.wsgi_request.user.is_anonymous) + # Response Content + size_limit = response.json()["upload-size-limit"] + self.assertEqual(size_limit["slug"], "some-size-limit") + self.assertEqual(size_limit["max_size"], 104857600) + self.assertEqual(size_limit["max_size_label"], "100.0\xa0MB") + + def test_patch_size_limit_admin_user(self): + url = reverse("upload-size-limits-detail", args=("some-size-limit",)) + + # List as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.patch(url, data={"max_size": 5242880}) + + # Assertions + self.assertEqual(response.status_code, 405) + self.assertEqual(response.wsgi_request.user, self.admin) + + def test_patch_size_limit_anonymous_user(self): + url = reverse("upload-size-limits-detail", args=("some-size-limit",)) + + # List as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.patch(url, data={"max_size": 2621440}) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) + + def test_put_size_limit_admin_user(self): + url = reverse("upload-size-limits-detail", args=("some-size-limit",)) + + # List as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.put(url, data={"slug": "some-size-limit", "max_size": 5242880}) + + # Assertions + self.assertEqual(response.status_code, 405) + self.assertEqual(response.wsgi_request.user, self.admin) + + def test_put_size_limit_anonymous_user(self): + url = reverse("upload-size-limits-detail", args=("some-size-limit",)) + + # List as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.put(url, data={"slug": "some-size-limit", "max_size": 2621440}) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) + + def test_post_size_limit_admin_user(self): + url = reverse("upload-size-limits-list") + + # List as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.post(url, data={"slug": "some-new-slug", "max_size": 5242880}) + + # Assertions + self.assertEqual(response.status_code, 201) + self.assertEqual(response.wsgi_request.user, self.admin) + # Response Content + size_limit = response.json()["upload-size-limit"] + self.assertEqual(size_limit["slug"], "some-new-slug") + self.assertEqual(size_limit["max_size"], 5242880) + self.assertEqual(size_limit["max_size_label"], "5.0\xa0MB") + + def test_post_size_limit_anonymous_user(self): + url = reverse("upload-size-limits-list") + + # List as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.post(url, data={"slug": "other-new-slug", "max_size": 2621440}) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) + + def test_delete_size_limit_admin_user(self): + url = reverse("upload-size-limits-detail", args=("some-size-limit",)) + + # List as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.delete(url) + + # Assertions + self.assertEqual(response.status_code, 405) + self.assertEqual(response.wsgi_request.user, self.admin) + + def test_delete_size_limit_anonymous_user(self): + url = reverse("upload-size-limits-detail", args=("some-other-size-limit",)) + + # List as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.delete(url) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) + + +class UploadParallelismLimitTests(APITestCase): + fixtures = [ + "group_test_data.json", + ] + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.admin = get_user_model().objects.get(username="admin") + cls.norman_user = get_user_model().objects.get(username="norman") + cls.test_user = get_user_model().objects.get(username="test_user") + try: + cls.default_parallelism_limit = UploadParallelismLimit.objects.get(slug="default_max_parallel_uploads") + except UploadParallelismLimit.DoesNotExist: + cls.default_parallelism_limit = UploadParallelismLimit.objects.create_default_limit() + + def test_list_parallelism_limits_admin_user(self): + url = reverse("upload-parallelism-limits-list") + + # List as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.get(url) + + # Assertions + self.assertEqual(response.status_code, 200) + self.assertEqual(response.wsgi_request.user, self.admin) + # Response Content + parallelism_limits = [ + (parallelism_limit["slug"], parallelism_limit["max_number"]) + for parallelism_limit in response.json()["upload-parallelism-limits"] + ] + expected_parallelism_limits = [ + (self.default_parallelism_limit.slug, self.default_parallelism_limit.max_number), + ] + for parallelism_limit in expected_parallelism_limits: + self.assertIn(parallelism_limit, parallelism_limits) + + def test_list_parallelism_limits_anonymous_user(self): + url = reverse("upload-parallelism-limits-list") + + # List as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.get(url) + + # Assertions + self.assertEqual(response.status_code, 200) + self.assertTrue(response.wsgi_request.user.is_anonymous) + # Response Content + parallelism_limits = [ + (parallelism_limit["slug"], parallelism_limit["max_number"]) + for parallelism_limit in response.json()["upload-parallelism-limits"] + ] + expected_parallelism_limits = [ + (self.default_parallelism_limit.slug, self.default_parallelism_limit.max_number), + ] + for parallelism_limit in expected_parallelism_limits: + self.assertIn(parallelism_limit, parallelism_limits) + + def test_retrieve_parallelism_limit_admin_user(self): + url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + + # Retrieve as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.get(url) + + # Assertions + self.assertEqual(response.status_code, 200) + self.assertEqual(response.wsgi_request.user, self.admin) + # Response Content + parallelism_limit = response.json()["upload-parallelism-limit"] + self.assertEqual(parallelism_limit["slug"], self.default_parallelism_limit.slug) + self.assertEqual(parallelism_limit["max_number"], self.default_parallelism_limit.max_number) + + def test_retrieve_parallelism_limit_norman_user(self): + url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + + # Retrieve as a norman user + self.client.force_authenticate(user=self.norman_user) + response = self.client.get(url) + + # Assertions + self.assertEqual(response.status_code, 200) + self.assertEqual(response.wsgi_request.user, self.norman_user) + # Response Content + parallelism_limit = response.json()["upload-parallelism-limit"] + self.assertEqual(parallelism_limit["slug"], self.default_parallelism_limit.slug) + self.assertEqual(parallelism_limit["max_number"], self.default_parallelism_limit.max_number) + + def test_patch_parallelism_limit_admin_user(self): + url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + + # Patch as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.patch(url, data={"max_number": 3}) + + # Assertions + self.assertEqual(response.status_code, 405) + self.assertEqual(response.wsgi_request.user, self.admin) + + def test_patch_parallelism_limit_norman_user(self): + url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + + # Patch as a norman user + self.client.force_authenticate(user=None) + response = self.client.patch(url, data={"max_number": 4}) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) + + def test_patch_parallelism_limit_anonymous_user(self): + url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + + # Patch as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.patch(url, data={"max_number": 6}) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) + + def test_put_parallelism_limit_admin_user(self): + url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + + # Put as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.put(url, data={"slug": self.default_parallelism_limit.slug, "max_number": 7}) + + # Assertions + self.assertEqual(response.status_code, 405) + self.assertEqual(response.wsgi_request.user, self.admin) + + def test_put_parallelism_limit_anonymous_user(self): + url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + + # Put as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.put(url, data={"slug": self.default_parallelism_limit.slug, "max_number": 8}) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) + + def test_post_parallelism_limit_admin_user(self): + url = reverse("upload-parallelism-limits-list") + + # Post as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.post(url, data={"slug": "some-parallelism-limit", "max_number": 9}) + + # Assertions + self.assertEqual(response.status_code, 201) + self.assertEqual(response.wsgi_request.user, self.admin) + # Response Content + parallelism_limit = response.json()["upload-parallelism-limit"] + self.assertEqual(parallelism_limit["slug"], "some-parallelism-limit") + self.assertEqual(parallelism_limit["max_number"], 9) + + def test_post_parallelism_limit_anonymous_user(self): + url = reverse("upload-parallelism-limits-list") + + # Post as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.post(url, data={"slug": "some-parallelism-limit", "max_number": 8}) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) + + def test_delete_parallelism_limit_admin_user(self): + UploadParallelismLimit.objects.create( + slug="test-parallelism-limit", + max_number=123, + ) + url = reverse("upload-parallelism-limits-detail", args=("test-parallelism-limit",)) + + # Delete as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.delete(url) + + # Assertions + self.assertEqual(response.status_code, 405) + self.assertEqual(response.wsgi_request.user, self.admin) + + def test_delete_parallelism_limit_admin_user_protected(self): + url = reverse("upload-parallelism-limits-detail", args=(self.default_parallelism_limit.slug,)) + + # Delete as an admin user + self.client.force_authenticate(user=self.admin) + response = self.client.delete(url) + + # Assertions + self.assertEqual(response.status_code, 405) + self.assertEqual(response.wsgi_request.user, self.admin) + + def test_delete_parallelism_limit_anonymous_user(self): + UploadParallelismLimit.objects.create( + slug="test-parallelism-limit", + max_number=123, + ) + url = reverse("upload-parallelism-limits-detail", args=("test-parallelism-limit",)) + + # Delete as an Anonymous user + self.client.force_authenticate(user=None) + response = self.client.delete(url) + + # Assertions + self.assertEqual(response.status_code, 403) + self.assertTrue(response.wsgi_request.user.is_anonymous) diff --git a/geonode/upload/api/urls.py b/geonode/upload/api/urls.py index 64fa3285c4c..a59a29f4b0c 100644 --- a/geonode/upload/api/urls.py +++ b/geonode/upload/api/urls.py @@ -16,12 +16,5 @@ # along with this program. If not, see . # ######################################################################### -from geonode.api.urls import router - -from . import views - -router.register(r"uploads", views.UploadViewSet, "uploads") -router.register(r"upload-size-limits", views.UploadSizeLimitViewSet, "upload-size-limits") -router.register(r"upload-parallelism-limits", views.UploadParallelismLimitViewSet, "upload-parallelism-limits") urlpatterns = [] diff --git a/geonode/upload/api/views.py b/geonode/upload/api/views.py index e8a659834c3..53f86bab425 100644 --- a/geonode/upload/api/views.py +++ b/geonode/upload/api/views.py @@ -16,65 +16,50 @@ # along with this program. If not, see . # ######################################################################### -from dynamic_rest.viewsets import DynamicModelViewSet +import logging +from urllib.parse import urljoin +from django.conf import settings +from django.http import Http404, HttpResponse +from django.urls import reverse +from pathlib import Path +from geonode.resource.enumerator import ExecutionRequestAction +from django.utils.translation import gettext_lazy as _ from dynamic_rest.filters import DynamicFilterBackend, DynamicSortingFilter - -from drf_spectacular.utils import extend_schema - -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response +from dynamic_rest.viewsets import DynamicModelViewSet +from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter, FavoriteFilter +from geonode.base.api.pagination import GeoNodeApiPagination +from geonode.base.api.permissions import ( + IsSelfOrAdminOrReadOnly, + ResourceBasePermissionsFilter, + UserHasPerms, +) from rest_framework.exceptions import ValidationError -from rest_framework.parsers import FileUploadParser -from rest_framework.permissions import IsAuthenticatedOrReadOnly -from rest_framework.authentication import SessionAuthentication, BasicAuthentication +from rest_framework import status +from geonode.base.api.serializers import ResourceBaseSerializer +from geonode.base.api.views import ResourceBaseViewSet +from geonode.base.models import ResourceBase +from geonode.storage.manager import StorageManager +from geonode.upload.api.permissions import UploadPermissionsFilter +from geonode.upload.models import UploadParallelismLimit, UploadSizeLimit +from geonode.upload.utils import UploadLimitValidator +from geonode.upload.api.exceptions import HandlerException, ImportException +from geonode.upload.api.serializer import ImporterSerializer +from geonode.upload.celery_tasks import import_orchestrator +from geonode.upload.orchestrator import orchestrator from oauth2_provider.contrib.rest_framework import OAuth2Authentication +from rest_framework.authentication import BasicAuthentication, SessionAuthentication +from rest_framework.parsers import FileUploadParser, MultiPartParser, JSONParser +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from geonode.assets.handlers import asset_handler_registry +from geonode.assets.local import LocalAssetHandler -from django.utils.translation import gettext_lazy as _ - -from geonode.base.api.filters import DynamicSearchFilter -from geonode.base.api.permissions import IsOwnerOrReadOnly, IsSelfOrAdminOrReadOnly -from geonode.base.api.pagination import GeoNodeApiPagination - - -from .serializers import ( - UploadSerializer, +from geonode.upload.api.serializer import ( UploadParallelismLimitSerializer, UploadSizeLimitSerializer, ) -from .permissions import UploadPermissionsFilter -from ..models import Upload, UploadParallelismLimit, UploadSizeLimit - -import logging - -logger = logging.getLogger(__name__) - - -class UploadViewSet(DynamicModelViewSet): - """ - API endpoint that allows uploads to be viewed or edited. - """ - - parser_class = [ - FileUploadParser, - ] - - authentication_classes = [SessionAuthentication, BasicAuthentication, OAuth2Authentication] - permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] - filter_backends = [DynamicFilterBackend, DynamicSortingFilter, DynamicSearchFilter, UploadPermissionsFilter] - queryset = Upload.objects.all() - serializer_class = UploadSerializer - pagination_class = GeoNodeApiPagination - http_method_names = ["get", "post"] - - @extend_schema( - methods=["post"], - responses={201: None}, - ) - @action(detail=False, methods=["post"]) - def upload(self, request, format=None): - return [] +logger = logging.getLogger("importer") class UploadSizeLimitViewSet(DynamicModelViewSet): @@ -120,3 +105,258 @@ def destroy(self, request, *args, **kwargs): raise ValidationError(detail) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) + + +class ImporterViewSet(DynamicModelViewSet): + """ + API endpoint that allows uploads to be viewed or edited. + """ + + parser_class = [JSONParser, FileUploadParser, MultiPartParser] + + authentication_classes = [ + BasicAuthentication, + SessionAuthentication, + OAuth2Authentication, + ] + permission_classes = [ + IsAuthenticatedOrReadOnly, + UserHasPerms(perms_dict={"default": {"POST": ["base.add_resourcebase"]}}), + ] + filter_backends = [ + DynamicFilterBackend, + DynamicSortingFilter, + DynamicSearchFilter, + UploadPermissionsFilter, + ] + queryset = ResourceBase.objects.all().order_by("-last_updated") + serializer_class = ImporterSerializer + pagination_class = GeoNodeApiPagination + http_method_names = ["get", "post"] + + def get_serializer_class(self): + specific_serializer = orchestrator.get_serializer(self.request.data) + return specific_serializer or ImporterSerializer + + def create(self, request, *args, **kwargs): + """ + Main function called by the new import flow. + It received the file via the front end + if is a gpkg (in future it will support all the vector file) + the new import flow is follow, else the normal upload api is used. + It clone on the local repo the file that the user want to upload + """ + _file = request.FILES.get("base_file") or request.data.get("base_file") + execution_id = None + asset_handler = LocalAssetHandler() + asset_dir = asset_handler._create_asset_dir() + + serializer = self.get_serializer_class() + data = serializer(data=request.data) + storage_manager = None + # serializer data validation + data.is_valid(raise_exception=True) + _data = { + **data.data.copy(), + **{key: value[0] if isinstance(value, list) else value for key, value in request.FILES.items()}, + } + + if "zip_file" in _data or "kmz_file" in _data: + # if a zipfile is provided, we need to unzip it before searching for an handler + zipname = Path(_data["base_file"].name).stem + storage_manager = StorageManager(remote_files={"base_file": _data.get("zip_file", _data.get("kmz_file"))}) + # cloning and unzip the base_file + storage_manager.clone_remote_files(cloning_directory=asset_dir, create_tempdir=False) + # update the payload with the unziped paths + _data.update( + { + **{"original_zip_name": zipname}, + **storage_manager.get_retrieved_paths(), + } + ) + + handler = orchestrator.get_handler(_data) + + # not file but handler means that is a remote resource + if handler: + asset = None + files = [] + try: + # cloning data into a local folder + extracted_params, _data = handler.extract_params_from_data(_data) + if _file: + storage_manager, asset, files = self._handle_asset( + request, asset_dir, storage_manager, _data, handler + ) + + self.validate_upload(request, storage_manager) + + action = ExecutionRequestAction.IMPORT.value + + input_params = { + **{"files": files, "handler_module_path": str(handler)}, + **extracted_params, + } + + if asset: + input_params.update( + { + "asset_id": asset.id, + "asset_module_path": f"{asset.__module__}.{asset.__class__.__name__}", + } + ) + + execution_id = orchestrator.create_execution_request( + user=request.user, + func_name=next(iter(handler.get_task_list(action=action))), + step=_(next(iter(handler.get_task_list(action=action)))), + input_params=input_params, + action=action, + name=_file.name if _file else extracted_params.get("title", None), + source=extracted_params.get("source"), + ) + + sig = import_orchestrator.s(files, str(execution_id), handler=str(handler), action=action) + sig.apply_async() + return Response(data={"execution_id": execution_id}, status=201) + except Exception as e: + # in case of any exception, is better to delete the + # cloned files to keep the storage under control + if asset: + try: + asset.delete() + except Exception as _exc: + logger.warning(_exc) + elif storage_manager is not None: + storage_manager.delete_retrieved_paths(force=True) + if execution_id: + orchestrator.set_as_failed(execution_id=str(execution_id), reason=e) + logger.exception(e) + raise ImportException(detail=e.args[0] if len(e.args) > 0 else e) + + raise ImportException(detail="No handlers found for this dataset type") + + def _handle_asset(self, request, asset_dir, storage_manager, _data, handler): + if storage_manager is None: + # means that the storage manager is not initialized yet, so + # the file is not a zip + storage_manager = StorageManager(remote_files=_data) + storage_manager.clone_remote_files(cloning_directory=asset_dir, create_tempdir=False) + # get filepath + asset, files = self.generate_asset_and_retrieve_paths(request, storage_manager, handler) + return storage_manager, asset, files + + def validate_upload(self, request, storage_manager): + upload_validator = UploadLimitValidator(request.user) + upload_validator.validate_parallelism_limit_per_user() + upload_validator.validate_files_sum_of_sizes(storage_manager.data_retriever) + + def generate_asset_and_retrieve_paths(self, request, storage_manager, handler): + asset_handler = asset_handler_registry.get_default_handler() + _files = storage_manager.get_retrieved_paths() + asset = asset_handler.create( + title="Original", + owner=request.user, + description=None, + type=handler.id, + files=list(set(_files.values())), + clone_files=False, + ) + + return asset, _files + + +class ResourceImporter(DynamicModelViewSet): + authentication_classes = [ + SessionAuthentication, + BasicAuthentication, + OAuth2Authentication, + ] + permission_classes = [ + IsAuthenticatedOrReadOnly, + UserHasPerms( + perms_dict={ + "dataset": { + "PUT": ["base.add_resourcebase", "base.download_resourcebase"], + "rule": all, + }, + "document": { + "PUT": ["base.add_resourcebase", "base.download_resourcebase"], + "rule": all, + }, + "default": {"PUT": ["base.add_resourcebase"]}, + } + ), + ] + filter_backends = [ + DynamicFilterBackend, + DynamicSortingFilter, + DynamicSearchFilter, + ExtentFilter, + ResourceBasePermissionsFilter, + FavoriteFilter, + ] + queryset = ResourceBase.objects.all().order_by("-last_updated") + serializer_class = ResourceBaseSerializer + pagination_class = GeoNodeApiPagination + + def copy(self, request, *args, **kwargs): + try: + resource = self.get_object() + if resource.resourcehandlerinfo_set.exists(): + handler_module_path = resource.resourcehandlerinfo_set.first().handler_module_path + + action = ExecutionRequestAction.COPY.value + + handler = orchestrator.load_handler(handler_module_path) + + if not handler.can_do(action): + raise HandlerException( + detail=f"The handler {handler_module_path} cannot manage the action required: {action}" + ) + + step = next(iter(handler.get_task_list(action=action))) + + extracted_params, _data = handler.extract_params_from_data(request.data, action=action) + + execution_id = orchestrator.create_execution_request( + user=request.user, + func_name=step, + step=step, + action=action, + input_params={ + **{"handler_module_path": handler_module_path}, + **extracted_params, + }, + source="importer_copy", + ) + + sig = import_orchestrator.s( + {}, + str(execution_id), + step=step, + handler=str(handler_module_path), + action=action, + layer_name=resource.title, + alternate=resource.alternate, + ) + sig.apply_async() + + # to reduce the work on the FE, the old payload is mantained + return Response( + data={ + "status": "ready", + "execution_id": execution_id, + "status_url": urljoin( + settings.SITEURL, + reverse("rs-execution-status", kwargs={"execution_id": execution_id}), + ), + }, + status=200, + ) + except (Exception, Http404) as e: + logger.error(e) + return HttpResponse(status=404, content=e) + return ResourceBaseViewSet(request=request, format_kwarg=None, args=args, kwargs=kwargs).resource_service_copy( + request, pk=kwargs.get("pk") + ) diff --git a/geonode/upload/apps.py b/geonode/upload/apps.py index 1730fa72957..1ac6c636aae 100644 --- a/geonode/upload/apps.py +++ b/geonode/upload/apps.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2024 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 @@ -16,16 +16,42 @@ # along with this program. If not, see . # ######################################################################### -from django.conf import settings from django.apps import AppConfig +from django.conf import settings class UploadAppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" name = "geonode.upload" def ready(self): - super().ready() + """Finalize setup""" + run_setup_hooks() + super(UploadAppConfig, self).ready() settings.CELERY_BEAT_SCHEDULE["clean-up-old-task-result"] = { "task": "geonode.upload.tasks.cleanup_celery_task_entries", "schedule": 86400.0, } + + +def run_setup_hooks(*args, **kwargs): + """ + Run basic setup configuration for the importer app. + Here we are overriding the upload API url + """ + from geonode.urls import urlpatterns + from django.urls import re_path, include + + url_already_injected = any( + [ + "geonode.upload.urls" in x.urlconf_name.__name__ + for x in urlpatterns + if hasattr(x, "urlconf_name") and not isinstance(x.urlconf_name, list) + ] + ) + + if not url_already_injected: + urlpatterns.insert( + 0, + re_path(r"^api/v2/", include("geonode.upload.api.urls")), + ) diff --git a/geonode/upload/celery_app.py b/geonode/upload/celery_app.py new file mode 100644 index 00000000000..f9d138f0aa4 --- /dev/null +++ b/geonode/upload/celery_app.py @@ -0,0 +1,29 @@ +######################################################################### +# +# Copyright (C) 2024 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 celery import Celery + +""" +Basic Celery app defined for the geonode.upload. +It read all the other settings from the django configuration file +so is always aligned to the geonode settings +""" + +importer_app = Celery("importer") + +importer_app.config_from_object("django.conf:settings", namespace="CELERY") diff --git a/geonode/upload/celery_tasks.py b/geonode/upload/celery_tasks.py new file mode 100644 index 00000000000..6a270d70133 --- /dev/null +++ b/geonode/upload/celery_tasks.py @@ -0,0 +1,773 @@ +######################################################################### +# +# Copyright (C) 2024 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 +import os +from typing import Optional + +from celery import Task +from django.db import connections, transaction +from django.utils import timezone +from django.utils.module_loading import import_string +from django.utils.translation import gettext_lazy +from dynamic_models.exceptions import DynamicModelError, InvalidFieldNameError +from dynamic_models.models import FieldSchema, ModelSchema +from geonode.base.models import ResourceBase +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.api.exceptions import ( + CopyResourceException, + InvalidInputFileException, + PublishResourceException, + ResourceCreationException, + StartImportException, +) +from geonode.upload.celery_app import importer_app +from geonode.upload.datastore import DataStoreManager +from geonode.upload.handlers.gpkg.tasks import SingleMessageErrorHandler +from geonode.upload.handlers.utils import ( + create_alternate, + drop_dynamic_model_schema, + evaluate_error, + get_uuid, +) +from geonode.upload.orchestrator import orchestrator +from geonode.upload.publisher import DataPublisher +from geonode.upload.settings import ( + IMPORTER_GLOBAL_RATE_LIMIT, + IMPORTER_PUBLISHING_RATE_LIMIT, + IMPORTER_RESOURCE_CREATION_RATE_LIMIT, +) +from geonode.upload.utils import call_rollback_function, error_handler, find_key_recursively + +logger = logging.getLogger("importer") + + +class ErrorBaseTaskClass(Task): + """ + Basic Error task class. Is common to all the base tasks of the import pahse + it defines a on_failure method which set the task as "failed" with some extra information + """ + + max_retries = 3 + track_started = True + + def on_failure(self, exc, task_id, args, kwargs, einfo): + # exc (Exception) - The exception raised by the task. + # args (Tuple) - Original arguments for the task that failed. + # kwargs (Dict) - Original keyword arguments for the task that failed. + evaluate_error(self, exc, task_id, args, kwargs, einfo) + + +@importer_app.task( + bind=True, + base=ErrorBaseTaskClass, + name="geonode.upload.import_orchestrator", + queue="geonode.upload.import_orchestrator", + max_retries=1, + rate_limit=IMPORTER_GLOBAL_RATE_LIMIT, + task_track_started=True, +) +def import_orchestrator( + self, + files: dict, + execution_id: str, + handler=None, + step="start_import", + layer_name=None, + alternate=None, + action=exa.IMPORT.value, + **kwargs, +): + """ + Base task. Is the task responsible to call the orchestrator and redirect the upload to the next step + mainly is a wrapper for the Orchestrator object. + + Parameters: + user (UserModel): user that is performing the request + execution_id (UUID): unique ID used to keep track of the execution request + step (str): last step performed from the tasks + layer_name (str): layer name + alternate (str): alternate used to naming the layer + Returns: + None + """ + try: + # extract the resource_type of the layer and retrieve the expected handler + + orchestrator.perform_next_step( + execution_id=execution_id, + step=step, + layer_name=layer_name, + alternate=alternate, + handler_module_path=handler, + action=action, + kwargs=kwargs, + ) + + except Exception as e: + raise StartImportException(detail=error_handler(e, execution_id)) + + +@importer_app.task( + bind=True, + # base=ErrorBaseTaskClass, + name="geonode.upload.import_resource", + queue="geonode.upload.import_resource", + max_retries=1, + rate_limit=IMPORTER_GLOBAL_RATE_LIMIT, + ignore_result=False, + task_track_started=True, +) +def import_resource(self, execution_id, /, handler_module_path, action, **kwargs): + """ + Task to import the resources. + NOTE: A validation if done before acutally start the import + + Parameters: + execution_id (UUID): unique ID used to keep track of the execution request + resource_type (str): extension of the resource type that we want to import + The resource type is needed to retrieve the right handler for the resource + Returns: + None + """ + # Updating status to running + try: + orchestrator.update_execution_request_status( + execution_id=execution_id, + last_updated=timezone.now(), + func_name="import_resource", + step=gettext_lazy("geonode.upload.import_resource"), + celery_task_request=self.request, + ) + _exec = orchestrator.get_execution_object(execution_id) + + _files = _exec.input_params.get("files") + + # initiating the data store manager + _datastore = DataStoreManager(_files, handler_module_path, _exec.user, execution_id) + + _datastore.pre_validation(**kwargs) + # starting file validation + if not _datastore.input_is_valid(): + raise Exception("dataset is invalid") + + _datastore.prepare_import(**kwargs) + _datastore.start_import(execution_id, **kwargs) + + """ + since the call to the orchestrator can changed based on the handler + called. See the GPKG handler gpkg_next_step task + """ + return self.name, execution_id + + except Exception as e: + call_rollback_function( + execution_id, + handlers_module_path=handler_module_path, + prev_action=exa.IMPORT.value, + layer=None, + alternate=None, + error=e, + **kwargs, + ) + raise InvalidInputFileException(detail=error_handler(e, execution_id)) + + +@importer_app.task( + bind=True, + base=ErrorBaseTaskClass, + name="geonode.upload.publish_resource", + queue="geonode.upload.publish_resource", + max_retries=3, + rate_limit=IMPORTER_PUBLISHING_RATE_LIMIT, + ignore_result=False, + task_track_started=True, +) +def publish_resource( + self, + execution_id: str, + /, + step_name: str, + layer_name: Optional[str] = None, + alternate: Optional[str] = None, + handler_module_path: str = None, + action: str = None, + **kwargs, +): + """ + Task to publish a single resource in geoserver. + NOTE: If the layer should be overwritten, for now we are skipping this feature + geoserver is not ready yet + + Parameters: + execution_id (UUID): unique ID used to keep track of the execution request + step_name (str): step name example: geonode.upload.publish_resource + layer_name (UUID): name of the resource example: layer + alternate (UUID): alternate of the resource example: layer_alternate + Returns: + None + """ + # Updating status to running + try: + kwargs = kwargs.get("kwargs") if "kwargs" in kwargs else kwargs + + orchestrator.update_execution_request_status( + execution_id=execution_id, + last_updated=timezone.now(), + func_name="publish_resource", + step=gettext_lazy("geonode.upload.publish_resource"), + celery_task_request=self.request, + ) + _exec = orchestrator.get_execution_object(execution_id) + _files = _exec.input_params.get("files") + _overwrite = _exec.input_params.get("overwrite_existing_layer") + + _publisher = DataPublisher(handler_module_path) + + # extracting the crs and the resource name, are needed for publish the resource + data = _publisher.extract_resource_to_publish(_files, action, layer_name, alternate, **kwargs) + if data: + # we should not publish resource without a crs + if not _overwrite or (_overwrite and not _publisher.get_resource(alternate)): + _publisher.publish_resources(data) + else: + _publisher.overwrite_resources(data) + + # updating the execution request status + orchestrator.update_execution_request_status( + execution_id=execution_id, + last_updated=timezone.now(), + celery_task_request=self.request, + ) + else: + logger.error( + f"Layer: {alternate} raised: Only resources with a CRS provided can be published for execution_id: {execution_id}" + ) + raise PublishResourceException("Only resources with a CRS provided can be published") + + # at the end recall the import_orchestrator for the next step + + task_params = ( + {}, + execution_id, + handler_module_path, + step_name, + layer_name, + alternate, + action, + ) + # for some reason celery will always put the kwargs into a key kwargs + # so we need to remove it + + import_orchestrator.apply_async(task_params, kwargs) + + return self.name, execution_id + + except Exception as e: + call_rollback_function( + execution_id, + handlers_module_path=handler_module_path, + prev_action=action, + layer=layer_name, + alternate=alternate, + error=e, + **kwargs, + ) + raise PublishResourceException(detail=error_handler(e, execution_id)) + + +@importer_app.task( + bind=True, + base=ErrorBaseTaskClass, + name="geonode.upload.create_geonode_resource", + queue="geonode.upload.create_geonode_resource", + max_retries=1, + rate_limit=IMPORTER_RESOURCE_CREATION_RATE_LIMIT, + ignore_result=False, + task_track_started=True, +) +def create_geonode_resource( + self, + execution_id: str, + /, + step_name: str, + layer_name: Optional[str] = None, + alternate: Optional[str] = None, + handler_module_path: str = None, + action: str = exa.IMPORT.value, + **kwargs, +): + """ + Create the GeoNode resource and the relatives information associated + NOTE: for gpkg we dont want to handle sld and XML files + + Parameters: + execution_id (UUID): unique ID used to keep track of the execution request + resource_type (str): extension of the resource type that we want to import + The resource type is needed to retrieve the right handler for the resource + step_name (str): step name example: geonode.upload.publish_resource + layer_name (UUID): name of the resource example: layer + alternate (UUID): alternate of the resource example: layer_alternate + Returns: + None + """ + # Updating status to running + try: + orchestrator.update_execution_request_status( + execution_id=execution_id, + last_updated=timezone.now(), + func_name="create_geonode_resource", + step=gettext_lazy("geonode.upload.create_geonode_resource"), + celery_task_request=self.request, + ) + _exec = orchestrator.get_execution_object(execution_id) + + _files = _exec.input_params.get("files") + + if not _files: + _asset = None + else: + _asset = ( + import_string(_exec.input_params.get("asset_module_path")) + .objects.filter(id=_exec.input_params.get("asset_id")) + .first() + ) + + handler_module_path = handler_module_path or _exec.input_params.get("handler_module_path") + + handler = import_string(handler_module_path)() + _overwrite = _exec.input_params.get("overwrite_existing_layer") + + if _overwrite: + resource = handler.overwrite_geonode_resource( + layer_name=layer_name, + alternate=alternate, + execution_id=execution_id, + asset=_asset, + ) + else: + resource = handler.create_geonode_resource( + layer_name=layer_name, + alternate=alternate, + execution_id=execution_id, + asset=_asset, + ) + + # assign geonode resource to ExectionRequest + orchestrator.update_execution_request_obj(_exec, {"geonode_resource": resource}) + + if _overwrite: + handler.overwrite_resourcehandlerinfo(handler_module_path, resource, _exec, **kwargs) + else: + handler.create_resourcehandlerinfo(handler_module_path, resource, _exec, **kwargs) + + # at the end recall the import_orchestrator for the next step + import_orchestrator.apply_async( + ( + _files, + execution_id, + handler_module_path, + step_name, + layer_name, + alternate, + action, + ) + ) + return self.name, execution_id + + except Exception as e: + call_rollback_function( + execution_id, + handlers_module_path=handler_module_path, + prev_action=action, + layer=layer_name, + alternate=alternate, + error=e, + **kwargs, + ) + raise ResourceCreationException(detail=error_handler(e)) + + +@importer_app.task( + base=ErrorBaseTaskClass, + name="geonode.upload.copy_geonode_resource", + queue="geonode.upload.copy_geonode_resource", + max_retries=1, + rate_limit=IMPORTER_RESOURCE_CREATION_RATE_LIMIT, + ignore_result=False, + task_track_started=True, +) +def copy_geonode_resource(exec_id, actual_step, layer_name, alternate, handler_module_path, action, **kwargs): + """ + Copy the geonode resource and create a new one. an assert is performed to be sure that the new resource + have the new generated alternate + """ + orchestrator.update_execution_request_status( + execution_id=exec_id, + last_updated=timezone.now(), + func_name="copy_geonode_resource", + step=gettext_lazy("geonode.upload.copy_geonode_resource"), + ) + original_dataset_alternate = kwargs.get("kwargs").get("original_dataset_alternate") + new_alternate = kwargs.get("kwargs").get("new_dataset_alternate") + from geonode.upload.celery_tasks import import_orchestrator + + try: + resource = ResourceBase.objects.filter(alternate=original_dataset_alternate) + if not resource.exists(): + raise Exception("The resource requested does not exists") + resource = resource.first() + + _exec = orchestrator.get_execution_object(exec_id) + + workspace = resource.alternate.split(":")[0] + + data_to_update = { + "alternate": f"{workspace}:{new_alternate}", + "name": new_alternate, + } + + if _exec.input_params.get("title"): + data_to_update["title"] = _exec.input_params.get("title") + + handler = import_string(handler_module_path)() + + new_resource = handler.copy_geonode_resource( + alternate=alternate, + resource=resource, + _exec=_exec, + data_to_update=data_to_update, + new_alternate=new_alternate, + **kwargs, + ) + + handler.create_resourcehandlerinfo( + resource=new_resource, + handler_module_path=handler_module_path, + execution_id=_exec, + ) + + assert f"{workspace}:{new_alternate}" == new_resource.alternate + + orchestrator.update_execution_request_status( + execution_id=str(_exec.exec_id), + input_params={**_exec.input_params, **{"instance": resource.pk}}, + output_params={"output": {"uuid": str(new_resource.uuid)}}, + ) + + task_params = ( + {}, + exec_id, + handler_module_path, + actual_step, + layer_name, + new_alternate, + action, + ) + # for some reason celery will always put the kwargs into a key kwargs + # so we need to remove it + kwargs = kwargs.get("kwargs") if "kwargs" in kwargs else kwargs + + import_orchestrator.apply_async(task_params, kwargs) + + except Exception as e: + call_rollback_function( + exec_id, + handlers_module_path=handler_module_path, + prev_action=action, + layer=layer_name, + alternate=alternate, + error=e, + **kwargs, + ) + raise CopyResourceException(detail=e) + return exec_id, new_alternate + + +@importer_app.task( + base=SingleMessageErrorHandler, + name="geonode.upload.create_dynamic_structure", + queue="geonode.upload.create_dynamic_structure", + max_retries=1, + acks_late=False, + ignore_result=False, + task_track_started=True, +) +def create_dynamic_structure( + execution_id: str, + fields: dict, + dynamic_model_schema_id: int, + overwrite: bool, + layer_name: str, +): + def _create_field(dynamic_model_schema, field, _kwargs): + # common method to define the Field Schema object + return FieldSchema( + name=field["name"], + class_name=field["class_name"], + model_schema=dynamic_model_schema, + kwargs=_kwargs, + ) + + """ + Create the single dynamic model field for each layer. Is made by a batch of 30 field + """ + dynamic_model_schema = ModelSchema.objects.filter(id=dynamic_model_schema_id) + if not dynamic_model_schema.exists(): + raise DynamicModelError(f"The model with id {dynamic_model_schema_id} does not exists.") + + dynamic_model_schema = dynamic_model_schema.first() + + row_to_insert = [] + for field in fields: + # setup kwargs for the class provided + if field["class_name"] is None or field["name"] is None: + logger.error( + f"Error during the field creation. The field or class_name is None {field} for {layer_name} for execution {execution_id}" + ) + raise InvalidFieldNameError( + f"Error during the field creation. The field or class_name is None {field} for {layer_name} for execution {execution_id}" + ) + + _kwargs = {"null": field.get("null", True)} + if field["class_name"].endswith("CharField"): + _kwargs = {**_kwargs, **{"max_length": 255}} + + if field.get("dim", None) is not None: + # setting the dimension for the gemetry. So that we can handle also 3d geometries + _kwargs = {**_kwargs, **{"dim": field.get("dim")}} + + # if is a new creation we generate the field model from scratch + if not overwrite: + row_to_insert.append(_create_field(dynamic_model_schema, field, _kwargs)) + else: + # otherwise if is an overwrite, we update the existing one and create the one that does not exists + _field_exists = FieldSchema.objects.filter(name=field["name"], model_schema=dynamic_model_schema) + if _field_exists.exists(): + _field_exists.update( + class_name=field["class_name"], + model_schema=dynamic_model_schema, + kwargs=_kwargs, + ) + else: + row_to_insert.append(_create_field(dynamic_model_schema, field, _kwargs)) + + if row_to_insert: + # the build creation improves the overall permformance with the DB + FieldSchema.objects.bulk_create(row_to_insert, 30) + + del row_to_insert + return "dynamic_model", layer_name, execution_id + + +@importer_app.task( + base=ErrorBaseTaskClass, + name="geonode.upload.copy_dynamic_model", + queue="geonode.upload.copy_dynamic_model", + task_track_started=True, +) +def copy_dynamic_model(exec_id, actual_step, layer_name, alternate, handler_module_path, action, **kwargs): + """ + Once the base resource is copied, is time to copy also the dynamic model + """ + + from geonode.upload.celery_tasks import import_orchestrator + + try: + orchestrator.update_execution_request_status( + execution_id=exec_id, + last_updated=timezone.now(), + func_name="copy_dynamic_model", + step=gettext_lazy("geonode.upload.copy_dynamic_model"), + ) + additional_kwargs = {} + + resource = ResourceBase.objects.filter(alternate=alternate) + + if not resource.exists(): + raise Exception("The resource requested does not exists") + + resource = resource.first() + + new_dataset_alternate = create_alternate(resource.title, exec_id).lower() + + if os.getenv("IMPORTER_ENABLE_DYN_MODELS", False): + dynamic_schema = ModelSchema.objects.filter(name=alternate.split(":")[1]) + alternative_dynamic_schema = ModelSchema.objects.filter(name=new_dataset_alternate) + + if dynamic_schema.exists() and not alternative_dynamic_schema.exists(): + # Creating the dynamic schema object + new_schema = dynamic_schema.first() + new_schema.name = new_dataset_alternate + new_schema.db_table_name = new_dataset_alternate + new_schema.pk = None + new_schema.save() + # create the field_schema object + fields = [] + for field in dynamic_schema.first().fields.all(): + obj = field + obj.model_schema = new_schema + obj.pk = None + fields.append(obj) + + FieldSchema.objects.bulk_create(fields) + + additional_kwargs = { + "original_dataset_alternate": resource.alternate, + "new_dataset_alternate": new_dataset_alternate, + } + + task_params = ( + {}, + exec_id, + handler_module_path, + actual_step, + layer_name, + new_dataset_alternate, + action, + ) + + import_orchestrator.apply_async(task_params, additional_kwargs) + + except Exception as e: + call_rollback_function( + exec_id, + handlers_module_path=handler_module_path, + prev_action=action, + layer=layer_name, + alternate=alternate, + error=e, + **{**kwargs, **additional_kwargs}, + ) + raise CopyResourceException(detail=e) + return exec_id, kwargs + + +@importer_app.task( + base=ErrorBaseTaskClass, + name="geonode.upload.copy_geonode_data_table", + queue="geonode.upload.copy_geonode_data_table", + task_track_started=True, +) +def copy_geonode_data_table(exec_id, actual_step, layer_name, alternate, handlers_module_path, action, **kwargs): + """ + Once the base resource is copied, is time to copy also the dynamic model + """ + try: + orchestrator.update_execution_request_status( + execution_id=exec_id, + last_updated=timezone.now(), + func_name="copy_geonode_data_table", + step=gettext_lazy("geonode.upload.copy_geonode_data_table"), + ) + + original_dataset_alternate = kwargs.get("kwargs").get("original_dataset_alternate").split(":")[1] + + new_dataset_alternate = kwargs.get("kwargs").get("new_dataset_alternate") + + from geonode.upload.celery_tasks import import_orchestrator + + db_name = os.getenv("DEFAULT_BACKEND_DATASTORE", "datastore") + if os.getenv("IMPORTER_ENABLE_DYN_MODELS", False): + schema_exists = ModelSchema.objects.filter(name=new_dataset_alternate).first() + if schema_exists: + db_name = schema_exists.db_name + + with transaction.atomic(): + with connections[db_name].cursor() as cursor: + cursor.execute(f'CREATE TABLE {new_dataset_alternate} AS TABLE "{original_dataset_alternate}";') + + task_params = ( + {}, + exec_id, + handlers_module_path, + actual_step, + layer_name, + alternate, + action, + ) + + kwargs = kwargs.get("kwargs") if "kwargs" in kwargs else kwargs + + import_orchestrator.apply_async(task_params, kwargs) + + except Exception as e: + call_rollback_function( + exec_id, + handlers_module_path=handlers_module_path, + prev_action=action, + layer=layer_name, + alternate=alternate, + error=e, + **kwargs, + ) + raise CopyResourceException(detail=e) + return exec_id, kwargs + + +@importer_app.task( + bind=True, + base=ErrorBaseTaskClass, + queue="geonode.upload.rollback", + name="geonode.upload.rollback", + task_track_started=True, +) +def rollback(self, *args, **kwargs): + """ + Task used to rollback the partially imported resource + The handler must implement the code to rollback each step that + is declared + """ + + exec_id = get_uuid(args) + + logger.info(f"Calling rollback for execution_id {exec_id} in progress") + + exec_object = orchestrator.get_execution_object(exec_id) + rollback_from_step = exec_object.step + action_to_rollback = exec_object.action + handler_module_path = exec_object.input_params.get("handler_module_path") + + orchestrator.update_execution_request_status( + execution_id=exec_id, + last_updated=timezone.now(), + func_name="rollback", + step=gettext_lazy("geonode.upload.rollback"), + celery_task_request=self.request, + ) + + handler = import_string(handler_module_path)() + if exec_object.input_params.get("overwrite_existing_layer"): + logger.warning("Rollback is skipped for the overwrite") + else: + handler.rollback(exec_id, rollback_from_step, action_to_rollback, *args, **kwargs) + error = find_key_recursively(kwargs, "error") or "Some issue has occured, please check the logs" + logger.error(error) + orchestrator.set_as_failed(exec_id, reason=error, delete_file=False) + return exec_id, kwargs + + +@importer_app.task(name="dynamic_model_error_callback") +def dynamic_model_error_callback(*args, **kwargs): + # revert eventually the import in ogr2ogr or the creation of the model in case of failing + alternate = args[0].args[-1] + schema_model = ModelSchema.objects.filter(name=alternate).first() + if schema_model: + drop_dynamic_model_schema(schema_model) + + return "error" diff --git a/geonode/upload/datastore.py b/geonode/upload/datastore.py new file mode 100644 index 00000000000..a479a5773c3 --- /dev/null +++ b/geonode/upload/datastore.py @@ -0,0 +1,71 @@ +######################################################################### +# +# Copyright (C) 2024 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.module_loading import import_string +from django.contrib.auth import get_user_model + +from geonode.upload.orchestrator import orchestrator + + +class DataStoreManager: + """ + Utility object to invoke the right handler used to save the + resource in the datastore db + """ + + def __init__( + self, + files: list, + handler_module_path: str, + user: get_user_model(), # type: ignore + execution_id: str, + ) -> None: + self.files = files + self.handler = import_string(handler_module_path) + self.user = user + self.execution_id = execution_id + + def input_is_valid(self): + """ + Perform basic validation steps + """ + if self.files: + return self.handler.is_valid(self.files, self.user, execution_id=self.execution_id) + url = orchestrator.get_execution_object(exec_id=self.execution_id).input_params.get("url") + if url: + return self.handler.is_valid_url(url) + return False + + def pre_validation(self, **kwargs): + """ + Hook for let the handler prepare the data before the validation. + Maybe a file rename, assign the resource to the execution_id + """ + return self.handler().pre_validation(self.files, self.execution_id, **kwargs) + + def prepare_import(self, **kwargs): + """ + prepares the data before the actual import + """ + return self.handler().prepare_import(self.files, self.execution_id, **kwargs) + + def start_import(self, execution_id, **kwargs): + """ + call the resource handler object to perform the import phase + """ + return self.handler().import_resource(self.files, execution_id, **kwargs) diff --git a/geonode/upload/db_router.py b/geonode/upload/db_router.py new file mode 100644 index 00000000000..1cd1f157404 --- /dev/null +++ b/geonode/upload/db_router.py @@ -0,0 +1,58 @@ +######################################################################### +# +# Copyright (C) 2022 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 . +# +######################################################################### +class DatastoreRouter: + """ + Router for redirect the resources into the datastore db + """ + + foi_model = { + "app_label": ["geonode_importer", "dynamic_models"], + } + + def db_for_read(self, model, **hints): + """ + Redirect to the datastore model for the FeatureOfInterest + """ + if model._meta.app_label in self.foi_model.get("app_label"): + return "datastore" + return None + + def db_for_write(self, model, **hints): + """ + Redirect to the datastore model for the FeatureOfInterest + """ + if model._meta.app_label in self.foi_model.get("app_label"): + return "datastore" + return None + + def allow_relation(self, obj1, obj2, **hints): + """ + Redirect to the datastore model for the FeatureOfInterest + """ + if obj1._meta.app_label in self.foi_model.get("app_label") or obj2._meta.app_label == "layer": + return True + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + """ + Redirect to the datastore model for the FeatureOfInterest + """ + if app_label in self.foi_model.get("app_label"): + return db == "datastore" + return None if db == "default" else False diff --git a/geonode/upload/files.py b/geonode/upload/files.py index 8cee7bbbc46..86cb0144765 100644 --- a/geonode/upload/files.py +++ b/geonode/upload/files.py @@ -23,21 +23,14 @@ @todo complete and use """ import re -import os -import os.path import logging -import zipfile from collections import UserList from geoserver.resource import FeatureType, Coverage -from django.utils.translation import gettext_lazy as _ -from django.core.exceptions import SuspiciousFileOperation -from geonode.utils import fixup_shp_columnnames -from geonode.storage.manager import storage_manager +logger = logging.getLogger("importer") -logger = logging.getLogger(__name__) vector = FeatureType.resource_type raster = Coverage.resource_type @@ -74,290 +67,3 @@ def all_files(self): def __repr__(self): return f"" - - -class FileType: - def __init__(self, name, code, dataset_type, aliases=None, auxillary_file_exts=None): - self.name = name - self.code = code - self.dataset_type = dataset_type - self.aliases = list(aliases) if aliases is not None else [] - self.auxillary_file_exts = list(auxillary_file_exts) if auxillary_file_exts is not None else [] - - def matches(self, ext): - ext = ext.lower() - return ext == self.code or ext in self.aliases - - def build_spatial_file(self, base, others): - aux_files, slds, xmls = self.find_auxillary_files(base, others) - - return SpatialFile(file_type=self, base_file=base, auxillary_files=aux_files, sld_files=slds, xml_files=xmls) - - def find_auxillary_files(self, base, others): - base_name = os.path.splitext(base)[0] - base_matches = [f for f in others if os.path.splitext(f)[0] == base_name] - slds = _find_file_type(base_matches, extension=".sld") - aux_files = [f for f in others if os.path.splitext(f)[1][1:].lower() in self.auxillary_file_exts] - xmls = _find_file_type(base_matches, extension=".xml") - return aux_files, slds, xmls - - def __repr__(self): - return f"" - - -TYPE_UNKNOWN = FileType("unknown", None, None) - -_keep_original_data = ("kmz", "zip-mosaic") -_tif_extensions = ("tif", "tiff", "geotif", "geotiff") -_mosaics_extensions = ("properties", "shp", "aux") - -types = [ - FileType( - "Shapefile", - "shp", - vector, - auxillary_file_exts=( - "dbf", - "shx", - "prj", - ), - ), - FileType("GeoTIFF", _tif_extensions[0], raster, aliases=_tif_extensions[1:]), - FileType( - "ImageMosaic", - "zip-mosaic", - raster, - aliases=_tif_extensions, - auxillary_file_exts=_mosaics_extensions + _tif_extensions, - ), - FileType("ASCII Text File", "asc", raster, auxillary_file_exts=("prj",)), - # requires geoserver importer extension - FileType("PNG", "png", raster, auxillary_file_exts=("prj",)), - FileType("JPG", "jpg", raster, auxillary_file_exts=("prj",)), - FileType("CSV", "csv", vector), - FileType("GeoJSON", "geojson", vector), - FileType("KML", "kml", vector), - FileType( - "KML Ground Overlay", - "kml-overlay", - raster, - aliases=( - "kmz", - "kml", - ), - auxillary_file_exts=( - "png", - "gif", - "jpg", - ) - + _tif_extensions, - ), - # requires geoserver gdal extension - FileType("ERDASImg", "img", raster), - FileType("NITF", "ntf", raster, aliases=("nitf")), - FileType("CIB1", "i41", raster, aliases=("i42", "i43", "i44", "i45", "i46", "i47", "i48", "i49")), - FileType("CIB5", "i21", raster, aliases=("i22", "i23", "i24", "i25", "i26", "i27", "i28", "i29")), - FileType("CIB10", "i11", raster, aliases=("i12", "i13", "i14", "i15", "i16", "i17", "i18", "i19")), - FileType("GNC", "gn1", raster, aliases=("gn2", "gn3", "gn4", "gn5", "gn6", "gn7", "gn8", "gn9")), - FileType("JNC", "jn1", raster, aliases=("jn2", "jn3", "jn4", "jn5", "jn6", "jn7", "jn8", "jn9")), - FileType("ONC", "on1", raster, aliases=("on2", "on3", "on4", "on5", "on6", "on7", "on8", "on9")), - FileType("TPC", "tp1", raster, aliases=("tp2", "tp3", "tp4", "tp5", "tp6", "tp7", "tp8", "tp9")), - FileType("JOG", "ja1", raster, aliases=("ja2", "ja3", "ja4", "ja5", "ja6", "ja7", "ja8", "ja9")), - FileType("TLM100", "tc1", raster, aliases=("tc2", "tc3", "tc4", "tc5", "tc6", "tc7", "tc8", "tc9")), - FileType("TLM50", "tl1", raster, aliases=("tl2", "tl3", "tl4", "tl5", "tl6", "tl7", "tl8", "tl9")), - # requires gdal plugin for mrsid and jp2 - FileType("MrSID", "sid", raster, auxillary_file_exts=("sdw",)), - FileType("JP2", "jp2", raster), -] - - -def get_type(name): - try: - file_type = [t for t in types if t.name == name][0] - except IndexError: - file_type = None - return file_type - - -def _contains_bad_names(file_names): - """return True if the list of names contains a bad one""" - return any([xml_unsafe.search(f) for f in file_names]) - - -def _clean_string(str, regex=r"(^[^a-zA-Z\._]+)|([^a-zA-Z\._0-9]+)", replace="_"): - """ - Replaces a string that matches the regex with the replacement. - """ - regex = re.compile(regex) - - if str[0].isdigit(): - str = replace + str - - return regex.sub(replace, str) - - -def _rename_files(file_names): - files = [] - for f in file_names: - dirname, base_name = os.path.split(f) - if dirname and base_name: - safe = _clean_string(base_name) - if safe != base_name: - safe = os.path.join(dirname, safe) - os.rename(f, safe) - files.append(safe) - else: - files.append(f) - return files - - -def _find_file_type(file_names, extension): - """ - Returns files that end with the given extension from a list of file names. - """ - return [f for f in file_names if f.lower().endswith(extension)] - - -def clean_macosx_dir(file_names): - """ - Returns the files sans anything in a __MACOSX directory - """ - return [f for f in file_names if "__MACOSX" not in f] - - -def get_scan_hint(valid_extensions): - """Provide hint on the type of file being handled in the upload session. - - This function is useful mainly for those file types that can carry - either vector or raster formats, like the KML type. - """ - if "kml" in valid_extensions: - if len(valid_extensions) == 2 and valid_extensions[1] == "sld": - result = "kml" - else: - result = "kml-overlay" - elif "kmz" in valid_extensions: - result = "kmz" - elif "zip-mosaic" in valid_extensions: - result = "zip-mosaic" - else: - result = None - return result - - -def scan_file(file_name, scan_hint=None, charset=None): - """get a list of SpatialFiles for the provided file""" - if not os.path.exists(file_name): - try: - if not storage_manager.exists(file_name): - raise Exception(_("Could not access to uploaded data.")) - except SuspiciousFileOperation: - pass - - dirname = os.path.dirname(file_name) - paths = [] - if ( - zipfile.is_zipfile(file_name) - or len(os.path.splitext(file_name)) > 0 - and os.path.splitext(file_name)[1].lower() == ".zip" - ): - try: - paths, kept_zip = _process_zip(file_name, dirname, scan_hint=scan_hint, charset=charset) - archive = file_name if kept_zip else None - except Exception as e: - logger.debug(e) - archive = file_name - else: - for p in os.listdir(dirname): - _f = os.path.join(dirname, p) - try: - fixup_shp_columnnames(_f, charset) - except Exception as e: - logger.debug(e) - paths.append(_f) - archive = None - if paths is not None: - safe_paths = _rename_files(paths) - else: - safe_paths = [] - - found = [] - for file_type in types: - for path in safe_paths: - path_extension = os.path.splitext(path)[-1][1:] - hint_ok = scan_hint is None or file_type.code == scan_hint or scan_hint in file_type.aliases - if file_type.matches(path_extension) and hint_ok: - _f = file_type.build_spatial_file(path, safe_paths) - found_paths = [f.base_file for f in found] - if path not in found_paths: - found.append(_f) - - # detect xmls and assign if a single upload is found - xml_files = _find_file_type(safe_paths, extension=".xml") - if xml_files: - if len(found) == 1: - found[0].xml_files = xml_files - else: - raise Exception(_("One or more XML files was provided, but no matching files were found for them.")) - - # detect slds and assign if a single upload is found - sld_files = _find_file_type(safe_paths, extension=".sld") - if sld_files: - if len(found) == 1: - found[0].sld_files = sld_files - else: - raise Exception(_("One or more SLD files was provided, but no matching files were found for them.")) - return SpatialFiles(dirname, found, archive=archive) - - -def _process_zip(zip_path, destination_dir, scan_hint=None, charset=None): - """Perform sanity checks on uploaded zip file - - This function will check if the zip file's contents have legal names. - If they do the zipfile remains compressed. Otherwise, it is extracted and - the files are renamed. - - It will also check if an .sld file exists inside the zip and extract it - - """ - safe_zip_path = _rename_files([zip_path])[0] - with zipfile.ZipFile(safe_zip_path, "r", allowZip64=True) as zip_handler: - if scan_hint in _keep_original_data: - extracted_paths = _extract_zip(zip_handler, destination_dir, charset) - else: - extracted_paths = _sanitize_zip_contents(zip_handler, destination_dir, charset) - if extracted_paths is not None: - all_paths = extracted_paths - kept_zip = False - else: - kept_zip = True - all_paths = [zip_path] - sld_paths = _probe_zip_for_sld(zip_handler, destination_dir) - all_paths.extend(sld_paths) - return all_paths, kept_zip - - -def _sanitize_zip_contents(zip_handler, destination_dir, charset): - clean_macosx_dir(zip_handler.namelist()) - result = _extract_zip(zip_handler, destination_dir, charset) - return result - - -def _extract_zip(zip_handler, destination, charset): - file_names = zip_handler.namelist() - zip_handler.extractall(destination) - paths = [] - for p in file_names: - _f = os.path.join(destination, p) - fixup_shp_columnnames(_f, charset) - paths.append(_f) - return paths - - -def _probe_zip_for_sld(zip_handler, destination_dir): - file_names = clean_macosx_dir(zip_handler.namelist()) - result = [] - for f in _find_file_type(file_names, extension=".sld"): - zip_handler.extract(f, destination_dir) - result.append(os.path.join(destination_dir, f)) - return result diff --git a/geonode/upload/forms.py b/geonode/upload/forms.py deleted file mode 100644 index d2a83e26a08..00000000000 --- a/geonode/upload/forms.py +++ /dev/null @@ -1,95 +0,0 @@ -######################################################################### -# -# Copyright (C) 2018 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 import forms -from django.core.exceptions import ValidationError - -logger = logging.getLogger(__name__) - - -class TimeForm(forms.Form): - presentation_strategy = forms.CharField(required=False) - precision_value = forms.IntegerField(required=False) - precision_step = forms.ChoiceField( - required=False, - choices=[("years",) * 2, ("months",) * 2, ("days",) * 2, ("hours",) * 2, ("minutes",) * 2, ("seconds",) * 2], - ) - - def __init__(self, *args, **kwargs): - # have to remove these from kwargs or Form gets mad - self._time_names = kwargs.pop("time_names", None) - self._text_names = kwargs.pop("text_names", None) - self._year_names = kwargs.pop("year_names", None) - super().__init__(*args, **kwargs) - self._build_choice("time_attribute", self._time_names) - self._build_choice("end_time_attribute", self._time_names) - self._build_choice("text_attribute", self._text_names) - self._build_choice("end_text_attribute", self._text_names) - widget = forms.TextInput(attrs={"placeholder": "Custom Format"}) - if self._text_names: - self.fields["text_attribute_format"] = forms.CharField(required=False, widget=widget) - self.fields["end_text_attribute_format"] = forms.CharField(required=False, widget=widget) - self._build_choice("year_attribute", self._year_names) - self._build_choice("end_year_attribute", self._year_names) - - def _resolve_attribute_and_type(self, *name_and_types): - return [(self.cleaned_data[n], t) for n, t in name_and_types if self.cleaned_data.get(n, None)] - - def _build_choice(self, att, names): - if names: - names.sort() - choices = [("", "")] + [(a, a) for a in names] - self.fields[att] = forms.ChoiceField(choices=choices, required=False) - - @property - def time_names(self): - return self._time_names - - @property - def text_names(self): - return self._text_names - - @property - def year_names(self): - return self._year_names - - def clean(self): - starts = self._resolve_attribute_and_type( - ("time_attribute", "Date"), - ("text_attribute", "Text"), - ("year_attribute", "Number"), - ) - if len(starts) > 1: - raise ValidationError("multiple start attributes") - ends = self._resolve_attribute_and_type( - ("end_time_attribute", "Date"), - ("end_text_attribute", "Text"), - ("end_year_attribute", "Number"), - ) - if len(ends) > 1: - raise ValidationError("multiple end attributes") - if len(starts) > 0: - self.cleaned_data["start_attribute"] = starts[0] - if len(ends) > 0: - self.cleaned_data["end_attribute"] = ends[0] - return self.cleaned_data - - # @todo implement clean diff --git a/geonode/upload/handlers/README.md b/geonode/upload/handlers/README.md new file mode 100644 index 00000000000..255c2b08331 --- /dev/null +++ b/geonode/upload/handlers/README.md @@ -0,0 +1,292 @@ +# Hanlder Definition + +As handler we define an object which is able to import a specific new resource in GeoNode. + +The main base code of each handler is defined under the `common` structure. +The `common` structure is meant to define the common step needed for each handler. + +For example for the `vector` file type, almost all the steps are in common. + +# How to create a new handler + +A new handler MUST implement the following function to make it works with the actual architecture. + +Follows a default handler structure complain with the importer architecture + +```python +import logging +from typing import List + +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.layers.models import Dataset +from geonode.upload.handlers.base import BaseHandler +from geonode.resource.models import ExecutionRequest + +logger = logging.getLogger("importer") + + + +class BaseVectorFileHandler(BaseHandler): + """ + Handler to import Vector files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: (), # define the list of the step (celery task) needed to execute the action for the resource + exa.COPY.value: (), + exa.DELETE.value: (), + exa.UPDATE.value: (), + } + + @property + def supported_file_extension_config(self): + ''' + Return the JSON configuration for the FE + needed to enable the new handler in the UI + ''' + return { + "id": "id", + "label": "label", + "format": "metadata", + "ext": ["ext"], + "optional": ["xml", "sld"], + } + + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Used in the import_resource step. It defines if the processed resource + can be considered valid or not. If not in the import_resource step + an exeption is raised + """ + return True + + @staticmethod + def can_handle(_data) -> bool: + """ + Used in the upload API. + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + return True + + @staticmethod + def has_serializer(_data) -> bool: + ''' + Used in the upload API. + This endpoint will return False if no custom serializer are defined, otherwise + it should return the serializer needed to validate the input API + Check the shapefile handler for more info + ''' + return False + + @staticmethod + def extract_params_from_data(_data, action=None): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + return + + @staticmethod + def publish_resources(resources: List[str], catalog, store, workspace): + """ + Given a list of strings (which rappresent the table name) + Will publish the resorces on geoserver + """ + return + + @staticmethod + def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate): + """ + Define the ogr2ogr command to be executed. + This is a default command that is needed to import a vector file. For Raster file + this function is not needed + """ + return + + @staticmethod + def delete_resource(instance): + """ + Base function to delete the resource with all the dependencies (example: dynamic model) + """ + return + + @staticmethod + def perform_last_step(execution_id): + ''' + Override this method if there is some extra step to perform + before considering the execution as completed. + For example can be used to trigger an email-send to notify + that the execution is completed + ''' + return + + def extract_resource_to_publish(self, files, action, layer_name, alternate, **kwargs): + """ + Function to extract the layer name and the CRS from needed in the + publishing phase + [ + {'name': 'alternate or layer_name', 'crs': 'EPSG:25832'} + ] + """ + return + + def get_ogr2ogr_driver(self): + """ + Should return the Driver object that is used to open the layers via OGR2OGR + """ + return None + + def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: + """ + Main function to import the resource. + Internally will call the steps required to import the + data inside the geonode_data database + """ + return + + def create_geonode_resource( + self, layer_name: str, alternate: str, execution_id: str, resource_type: Dataset = Dataset, files=None + ): + """ + Base function to create the resource into geonode. Each handler can specify + and handle the resource in a different way. Is highly suggested to use + the GeoNode resource_manager + """ + return + + def overwrite_geonode_resource( + self, layer_name: str, alternate: str, execution_id: str, resource_type: Dataset = Dataset, asset=None + ): + """ + Base function to override the resource into geonode. Each handler can specify + and handle the resource in a different way. Is highly suggested to use + the GeoNode resource_manager + """ + return + + def handle_xml_file(self, saved_dataset: Dataset, _exec: ExecutionRequest): + """ + Base function to import the XML within the resource. Each handler can specify + and handle the resource in a different way. Is highly suggested to use + the GeoNode resource_manager + """ + return + + def handle_sld_file(self, saved_dataset: Dataset, _exec: ExecutionRequest): + """ + Base function to import the SLD within the resource. Each handler can specify + and handle the resource in a different way. Is highly suggested to use + the GeoNode resource_manager + """ + return + + def create_resourcehandlerinfo(self, handler_module_path: str, resource: Dataset, execution_id: ExecutionRequest, **kwargs): + """ + Create relation between the GeonodeResource and the handler used + to create/copy it + """ + return + + def overwrite_resourcehandlerinfo(self, handler_module_path: str, resource: Dataset, execution_id: ExecutionRequest, **kwargs): + """ + Overwrite the ResourceHandlerInfo + """ + return + + def copy_geonode_resource( + self, alternate: str, resource: Dataset, _exec: ExecutionRequest, data_to_update: dict, new_alternate: str, **kwargs + ): + """ + Base function to copy already exists Geonode Resource. Each handler can specify + and handle the resource in a different way. Is highly suggested to use + the GeoNode resource_manager + """ + return + +``` + +### Additional info + +- `ACTION`: this property represents the list of the action (import/copy/etc..) that the handler can perform. Each `ACTION` must define the list of the celery task (steps) that the orchestrator must call to successfully import/copy/etc.. the resource. + +- `supported_file_extension_config`: Need to let show the new supported type in the front-end + + +# Using a common structure: + +Not all handlers must redefine the above structure. If the resource type is a `vector` file, the `common` file can be used to have a structure already in place, for example: + +```python +import logging + +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.upload.utils import UploadLimitValidator +from geopackage_validator.validate import validate +from geonode.upload.handlers.gpkg.exceptions import InvalidGeopackageException +from osgeo import ogr + +from geonode.upload.handlers.common.vector import BaseVectorFileHandler + +logger = logging.getLogger("importer") + + + +class NewVectorFileHandler(BaseVectorFileHandler): + """ + Handler to import GPK files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ), + } + + @property + def supported_file_extension_config(self): + return {"id": "gpkg", "label": "GeoPackage", "format": "archive", "ext": ["gpkg"]} + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + return + + @staticmethod + def is_valid(files, user, **kwargs): + return True + + def get_ogr2ogr_driver(self): + return ogr.GetDriverByName("GPKG") + +``` + + +# How to register the new handler + +Once the new handler is defined, it must be registered in the settings like a Django application: + +``` +IMPORTER_HANDLERS = os.getenv('IMPORTER_HANDLERS', [ + 'geonode.upload.handlers.gpkg.handler.GPKGFileHandler', + 'path.to.my.new.Handler.' <---- +]) +``` \ No newline at end of file diff --git a/geonode/upload/templatetags/__init__.py b/geonode/upload/handlers/__init__.py similarity index 96% rename from geonode/upload/templatetags/__init__.py rename to geonode/upload/handlers/__init__.py index 79177e00bdd..50b39831df7 100644 --- a/geonode/upload/templatetags/__init__.py +++ b/geonode/upload/handlers/__init__.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2024 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 diff --git a/geonode/upload/handlers/apps.py b/geonode/upload/handlers/apps.py new file mode 100644 index 00000000000..7e50e59aae5 --- /dev/null +++ b/geonode/upload/handlers/apps.py @@ -0,0 +1,61 @@ +######################################################################### +# +# Copyright (C) 2024 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.apps import AppConfig +from django.conf import settings +from django.utils.module_loading import import_string +from geonode.upload.settings import SYSTEM_HANDLERS + +logger = logging.getLogger("importer") + + +class HandlersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "geonode.upload.handlers" + + def ready(self): + """Finalize setup""" + run_setup_hooks() + super(HandlersConfig, self).ready() + + +def run_setup_hooks(*args, **kwargs): + available_handlers = settings.IMPORTER_HANDLERS + SYSTEM_HANDLERS + _handlers = [import_string(module_path) for module_path in available_handlers] + for item in _handlers: + item.register() + logger.info(f"The following handlers have been registered: {', '.join(available_handlers)}") + + _available_settings = [ + import_string(module_path)().supported_file_extension_config + for module_path in available_handlers + if import_string(module_path)().supported_file_extension_config + ] + # injecting the new config required for FE + supported_type = [] + supported_type.extend(_available_settings) + if not getattr(settings, "ADDITIONAL_DATASET_FILE_TYPES", None): + setattr(settings, "ADDITIONAL_DATASET_FILE_TYPES", supported_type) + elif "gpkg" not in [x.get("id") for x in settings.ADDITIONAL_DATASET_FILE_TYPES]: + settings.ADDITIONAL_DATASET_FILE_TYPES.extend(supported_type) + setattr( + settings, + "ADDITIONAL_DATASET_FILE_TYPES", + settings.ADDITIONAL_DATASET_FILE_TYPES, + ) diff --git a/geonode/upload/handlers/base.py b/geonode/upload/handlers/base.py new file mode 100644 index 00000000000..5afb57bbe76 --- /dev/null +++ b/geonode/upload/handlers/base.py @@ -0,0 +1,348 @@ +######################################################################### +# +# Copyright (C) 2024 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 abc import ABC +import logging +from typing import List + +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.layers.models import Dataset +from geonode.upload.api.exceptions import ImportException +from geonode.upload.utils import ImporterRequestAction as ira, find_key_recursively +from django_celery_results.models import TaskResult +from django.db.models import Q +from geonode.resource.models import ExecutionRequest +from geonode.base.models import ResourceBase + + +logger = logging.getLogger("importer") + + +class BaseHandler(ABC): + """ + Base abstract handler object + define the required method needed to define an upload handler + it must be: + - provide the tasks list to complete the import + - validation function + - method to import the resource + - create_error_log + """ + + REGISTRY = [] + + ACTIONS = { + exa.IMPORT.value: (), + exa.COPY.value: (), + exa.DELETE.value: (), + exa.UPDATE.value: (), + ira.ROLLBACK.value: (), + } + + def __str__(self): + return f"{self.__module__}.{self.__class__.__name__}" + + def __repr__(self): + return self.__str__() + + @classmethod + def register(cls): + BaseHandler.REGISTRY.append(cls) + + @classmethod + def get_registry(cls): + return BaseHandler.REGISTRY + + @classmethod + def get_task_list(cls, action) -> tuple: + if action not in cls.ACTIONS: + raise Exception("The requested action is not implemented yet") + return cls.ACTIONS.get(action) + + @property + def default_geometry_column_name(self): + return "geometry" + + @property + def id(self): + pk = self.supported_file_extension_config.get("id", None) + if pk is None: + raise ImportException( + "PK must be defined, check that supported_file_extension_config had been correctly defined, it cannot be empty" + ) + return pk + + @property + def supported_file_extension_config(self): + return {} + + @property + def can_handle_xml_file(self) -> bool: + """ + True or false if the handler is able to handle XML file + By default a common workflow is always defined + To be override if some expection are needed + """ + return True + + @property + def can_handle_sld_file(self) -> bool: + """ + True or false if the handler is able to handle SLD file + By default a common workflow is always defined + To be override if some expection are needed + """ + return True + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps + """ + return NotImplementedError + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + return False + + @staticmethod + def has_serializer(_data) -> bool: + """ + This endpoint should return (if set) the custom serializer used in the API + to validate the input resource + """ + return None + + @staticmethod + def can_do(action) -> bool: + """ + Evaluate if the handler can take care of a specific action. + Each action (import/copy/etc...) can define different step so + the Handler must be ready to handle them. If is not in the actual + flow the already in place flow is followd + """ + return action in BaseHandler.ACTIONS + + @staticmethod + def extract_params_from_data(_data): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + return [] + + @staticmethod + def perform_last_step(execution_id): + """ + Override this method if there is some extra step to perform + before considering the execution as completed. + For example can be used to trigger an email-send to notify + that the execution is completed + """ + from geonode.upload.orchestrator import orchestrator + from geonode.upload.models import ResourceHandlerInfo + + # as last step, we delete the celery task to keep the number of rows under control + lower_exec_id = execution_id.replace("-", "_").lower() + TaskResult.objects.filter( + Q(task_args__icontains=lower_exec_id) + | Q(task_kwargs__icontains=lower_exec_id) + | Q(result__icontains=lower_exec_id) + | Q(task_args__icontains=execution_id) + | Q(task_kwargs__icontains=execution_id) + | Q(result__icontains=execution_id) + ).delete() + + _exec = orchestrator.get_execution_object(execution_id) + + resource_output_params = [ + {"detail_url": x.resource.detail_url, "id": x.resource.pk} + for x in ResourceHandlerInfo.objects.filter(execution_request=_exec) + ] + _exec.output_params.update({"resources": resource_output_params}) + _exec.save() + + return _exec + + def fixup_name(self, name): + """ + Emulate the LAUNDER option in ogr2ogr which will normalize the string. + This is enriched with additional transformation for parentesis. + The basic normalized function can be found here + https://github.com/OSGeo/gdal/blob/0fc262675051b63f96c91ca920d27503655dfb7b/ogr/ogrsf_frmts/pgdump/ogrpgdumpdatasource.cpp#L130 # noqa + + We use replace because it looks to be one of the fasted options: + https://stackoverflow.com/questions/3411771/best-way-to-replace-multiple-characters-in-a-string + """ + return ( + name.lower() + .replace("-", "_") + .replace(" ", "_") + .replace("#", "_") + .replace("\\", "_") + .replace(".", "") + .replace(")", "") + .replace("(", "") + .replace(",", "") + .replace("&", "")[:62] + ) + + def extract_resource_to_publish(self, files, layer_name, alternate, **kwargs): + """ + Function to extract the layer name and the CRS from needed in the + publishing phase + [ + {'name': 'alternate or layer_name', 'crs': 'EPSG:25832'} + ] + """ + return NotImplementedError + + def overwrite_geoserver_resource(self, resource, catalog, store, workspace): + """ + Base method for override the geoserver resource. For vector file usually + is not needed since the value are replaced by ogr2ogr + """ + pass + + @staticmethod + def create_error_log(exc, task_name, *args): + """ + This function will handle the creation of the log error for each message. + This is helpful and needed, so each handler can specify the log as needed + """ + return f"Task: {task_name} raised an error during actions for layer: {args[-1]}: {exc}" + + def prepare_import(self, files, execution_id, **kwargs): + """ + Optional preparation step to before the actual import begins. + By default this does nothing. + """ + pass + + def import_resource(self, files: dict, execution_id: str, **kwargs): + """ + Define the step to perform the import of the data + into the datastore db + """ + return NotImplementedError + + @staticmethod + def publish_resources(resources: List[str], catalog, store, workspace): + """ + Given a list of strings (which rappresent the table on geoserver) + Will publish the resorces on geoserver + """ + return NotImplementedError + + def create_geonode_resource(self, layer_name, alternate, execution_id, resource_type: Dataset = Dataset): + """ + Base function to create the resource into geonode. Each handler can specify + and handle the resource in a different way + """ + return NotImplementedError + + def create_resourcehandlerinfo(self, handler_module_path, resource, **kwargs): + return NotImplementedError + + def get_ogr2ogr_task_group(self, execution_id, files, layer, should_be_overwritten, alternate): + """ + implement custom ogr2ogr task group + """ + return NotImplementedError + + @staticmethod + def delete_resource(instance): + """ + Base function to delete the resource with all the dependencies (example: dynamic model) + """ + return + + def _get_execution_request_object(self, execution_id: str): + return ExecutionRequest.objects.filter(exec_id=execution_id).first() + + def overwrite_resourcehandlerinfo( + self, + handler_module_path: str, + resource: Dataset, + execution_id: ExecutionRequest, + **kwargs, + ): + """ + Overwrite the ResourceHandlerInfo + """ + if resource.resourcehandlerinfo_set.exists(): + resource.resourcehandlerinfo_set.update( + handler_module_path=handler_module_path, + resource=resource, + execution_request=execution_id, + kwargs=kwargs.get("kwargs", {}) or kwargs, + ) + return + return self.create_resourcehandlerinfo(handler_module_path, resource, execution_id, **kwargs) + + def rollback(self, exec_id, rollback_from_step, action_to_rollback, *args, **kwargs): + steps = self.ACTIONS.get(action_to_rollback) + + if rollback_from_step not in steps: + logger.info(f"Step not found {rollback_from_step}, skipping") + return + step_index = steps.index(rollback_from_step) + # the start_import, start_copy etc.. dont do anything as step, is just the start + # so there is nothing to rollback + steps_to_rollback = steps[1 : step_index + 1] # noqa + if not steps_to_rollback: + return + # reversing the tuple to going backwards with the rollback + reversed_steps = steps_to_rollback[::-1] + instance_name = None + try: + instance_name = find_key_recursively(kwargs, "new_dataset_alternate") or args[3] + except Exception: + pass + + logger.warning(f"Starting rollback for execid: {exec_id} resource published was: {instance_name}") + + for step in reversed_steps: + normalized_step_name = step.split(".")[-1] + if getattr(self, f"_{normalized_step_name}_rollback", None): + function = getattr(self, f"_{normalized_step_name}_rollback") + function(exec_id, instance_name, *args, **kwargs) + + logger.warning(f"Rollback for execid: {exec_id} resource published was: {instance_name} completed") + + def _create_geonode_resource_rollback(self, exec_id, istance_name=None, *args, **kwargs): + from geonode.upload.orchestrator import orchestrator + + """ + The handler will remove the resource from geonode + """ + logger.info(f"Rollback geonode step in progress for execid: {exec_id} resource created was: {istance_name}") + _exec_obj = orchestrator.get_execution_object(exec_id) + resource = ResourceBase.objects.filter(alternate__icontains=istance_name, owner=_exec_obj.user) + if resource.exists(): + resource.delete() + + def _copy_dynamic_model_rollback(self, exec_id, istance_name=None, *args, **kwargs): + self._import_resource_rollback(exec_id, istance_name=istance_name) + + def _copy_geonode_resource_rollback(self, exec_id, istance_name=None, *args, **kwargs): + self._create_geonode_resource_rollback(exec_id, istance_name=istance_name) diff --git a/geonode/upload/handlers/common/__init__.py b/geonode/upload/handlers/common/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/common/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/common/metadata.py b/geonode/upload/handlers/common/metadata.py new file mode 100644 index 00000000000..35374a42a59 --- /dev/null +++ b/geonode/upload/handlers/common/metadata.py @@ -0,0 +1,131 @@ +######################################################################### +# +# Copyright (C) 2024 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 geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.handlers.base import BaseHandler +from geonode.upload.handlers.utils import UploadSourcesEnum +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload.handlers.xml.serializer import MetadataFileSerializer +from geonode.upload.utils import ImporterRequestAction as ira +from geonode.upload.orchestrator import orchestrator +from django.shortcuts import get_object_or_404 +from geonode.layers.models import Dataset + +logger = logging.getLogger("importer") + + +class MetadataFileHandler(BaseHandler): + """ + Handler to import metadata files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ("start_import", "geonode.upload.import_resource"), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + if _data.get("source", None) == UploadSourcesEnum.resource_file_upload.value: + return True + return False + + @staticmethod + def has_serializer(data) -> bool: + _base = data.get("base_file") + if not _base: + return False + if ( + _base.endswith("xml") or _base.endswith("sld") + if isinstance(_base, str) + else _base.name.endswith("xml") or _base.name.endswith("sld") + ): + return MetadataFileSerializer + return False + + @staticmethod + def extract_params_from_data(_data, action=None): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + return { + "dataset_title": _data.pop("dataset_title", None), + "skip_existing_layers": _data.pop("skip_existing_layers", "False"), + "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), + "resource_pk": _data.pop("resource_pk", None), + "store_spatial_file": _data.pop("store_spatial_files", "True"), + "source": _data.pop("source", "resource_file_upload"), + }, _data + + @staticmethod + def perform_last_step(execution_id): + BaseHandler.perform_last_step(execution_id=execution_id) + + def pre_validation(self, files, execution_id, **kwargs): + """ + Hook for let the handler prepare the data before the validation. + Maybe a file rename, assign the resource to the execution_id + """ + _exec = orchestrator.get_execution_object(exec_id=execution_id) + dataset = MetadataFileHandler()._get_resource(_exec) + # assign the resource to the execution_obj + orchestrator.update_execution_request_obj(_exec, {"geonode_resource": dataset}) + + def import_resource(self, files: dict, execution_id: str, **kwargs): + _exec = orchestrator.get_execution_object(execution_id) + # getting the dataset + dataset = self._get_resource(_exec) + + # retrieving the handler used for the dataset + original_handler = orchestrator.load_handler(dataset.resourcehandlerinfo_set.first().handler_module_path)() + + ResourceHandlerInfo.objects.create( + handler_module_path=dataset.resourcehandlerinfo_set.first().handler_module_path, + resource=dataset, + execution_request=_exec, + kwargs=kwargs.get("kwargs", {}) or kwargs, + ) + + self.handle_metadata_resource(_exec, dataset, original_handler) + + dataset.refresh_from_db() + + orchestrator.evaluate_execution_progress(execution_id, handler_module_path=str(self)) + return dataset + + def _get_resource(self, _exec): + pk = _exec.input_params.get("resource_pk") + resource_id = _exec.input_params.get("resource_id") + if resource_id: + dataset = get_object_or_404(Dataset, pk=resource_id) + elif pk: + dataset = get_object_or_404(Dataset, pk=pk) + return dataset + + def handle_metadata_resource(self, _exec, dataset, original_handler): + raise NotImplementedError diff --git a/geonode/upload/handlers/common/raster.py b/geonode/upload/handlers/common/raster.py new file mode 100644 index 00000000000..14dd5d40971 --- /dev/null +++ b/geonode/upload/handlers/common/raster.py @@ -0,0 +1,570 @@ +######################################################################### +# +# Copyright (C) 2024 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 pyproj +from geonode.upload.publisher import DataPublisher +import json +import logging +from pathlib import Path +from subprocess import PIPE, Popen +from typing import List + +from django.conf import settings +from django.db.models import Q +from geonode.base.models import ResourceBase +from geonode.layers.models import Dataset +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.resource.manager import resource_manager +from geonode.resource.models import ExecutionRequest +from geonode.upload.api.exceptions import ImportException +from geonode.upload.celery_tasks import ErrorBaseTaskClass, import_orchestrator +from geonode.upload.handlers.base import BaseHandler +from geonode.upload.handlers.geotiff.exceptions import InvalidGeoTiffException +from geonode.upload.handlers.utils import UploadSourcesEnum, create_alternate, should_be_imported +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload.orchestrator import orchestrator +from osgeo import gdal +from geonode.upload.celery_app import importer_app +from geonode.storage.manager import storage_manager + +logger = logging.getLogger("importer") + + +gdal.UseExceptions() + + +class BaseRasterFileHandler(BaseHandler): + """ + Handler to import Raster files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + @property + def default_geometry_column_name(self): + return "geometry" + + @property + def supported_file_extension_config(self): + return NotImplementedError + + @staticmethod + def get_geoserver_store_name(default=None): + """ + Method that return the base store name where to save the data in geoserver + and a boolean to know if the store should be created. + For raster, the store is created during the geoserver publishing + so we dont want to created it upfront + """ + return default, False + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps + """ + result = Popen("gdal_translate --version", stdout=PIPE, stderr=PIPE, shell=True) + _, stderr = result.communicate() + if stderr: + raise ImportException(stderr) + return True + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + if _data.get("source", None) != UploadSourcesEnum.upload.value: + return False + return True + + @staticmethod + def has_serializer(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + return False + + @staticmethod + def can_do(action) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + return action in BaseHandler.ACTIONS + + @staticmethod + def create_error_log(exc, task_name, *args): + """ + This function will handle the creation of the log error for each message. + This is helpful and needed, so each handler can specify the log as needed + """ + return f"Task: {task_name} raised an error during actions for layer: {args[-1]}: {exc}" + + @staticmethod + def extract_params_from_data(_data, action=None): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + if action == exa.COPY.value: + title = json.loads(_data.get("defaults")) + return {"title": title.pop("title")}, _data + + return { + "skip_existing_layers": _data.pop("skip_existing_layers", "False"), + "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), + "resource_pk": _data.pop("resource_pk", None), + "store_spatial_file": _data.pop("store_spatial_files", "True"), + "source": _data.pop("source", "upload"), + }, _data + + @staticmethod + def publish_resources(resources: List[str], catalog, store, workspace): + """ + Given a list of strings (which rappresent the table on geoserver) + Will publish the resorces on geoserver + """ + for _resource in resources: + try: + catalog.create_coveragestore( + _resource.get("name"), + path=_resource.get("raster_path"), + layer_name=_resource.get("name"), + workspace=workspace, + overwrite=True, + upload_data=False, + ) + except Exception as e: + if f"Resource named {_resource.get('name')} already exists in store:" in str(e): + continue + raise e + return True + + def pre_validation(self, files, execution_id, **kwargs): + """ + Hook for let the handler prepare the data before the validation. + Maybe a file rename, assign the resource to the execution_id + """ + + def overwrite_geoserver_resource(self, resource: List[str], catalog, store, workspace): + # we need to delete the resource before recreating it + self._delete_resource(resource, catalog, workspace) + self._delete_store(resource, catalog, workspace) + return self.publish_resources([resource], catalog, store, workspace) + + def _delete_store(self, resource, catalog, workspace): + store = None + possible_layer_name = [ + resource.get("name"), + resource.get("name").split(":")[-1], + f"{workspace.name}:{resource.get('name')}", + ] + for el in possible_layer_name: + store = catalog.get_store(el, workspace=workspace) + if store: + break + if store: + catalog.delete(store, purge="all", recurse=True) + return store + + def _delete_resource(self, resource, catalog, workspace): + res = None + possible_layer_name = [ + resource.get("name"), + resource.get("name").split(":")[-1], + f"{workspace.name}:{resource.get('name')}", + ] + for el in possible_layer_name: + res = catalog.get_resource(el, workspace=workspace) + if res: + break + if res: + catalog.delete(res, purge="all", recurse=True) + + @staticmethod + def delete_resource(instance): + # it should delete the image from the geoserver data dir + # for now we can rely on the geonode delete behaviour + # since the file is stored on local + pass + + @staticmethod + def perform_last_step(execution_id): + BaseHandler.perform_last_step(execution_id=execution_id) + + def extract_resource_to_publish(self, files, action, layer_name, alternate, **kwargs): + if action == exa.COPY.value: + return [ + { + "name": alternate, + "crs": ResourceBase.objects.filter( + Q(alternate__icontains=layer_name) | Q(title__icontains=layer_name) + ) + .first() + .srid, + "raster_path": kwargs["kwargs"].get("new_file_location").get("files")[0], + } + ] + + layers = gdal.Open(files.get("base_file")) + if not layers: + return [] + return [ + { + "name": alternate or layer_name, + "crs": (self.identify_authority(layers) if layers.GetSpatialRef() else None), + "raster_path": files.get("base_file"), + } + ] + + def identify_authority(self, layer): + try: + layer_wkt = layer.GetSpatialRef().ExportToWkt() + _name = "EPSG" + _code = pyproj.CRS(layer_wkt).to_epsg(min_confidence=20) + if _code is None: + layer_proj4 = layer.GetSpatialRef().ExportToProj4() + _code = pyproj.CRS(layer_proj4).to_epsg(min_confidence=20) + if _code is None: + raise Exception("CRS authority code not found, fallback to default behaviour") + except Exception: + spatial_ref = layer.GetSpatialRef() + spatial_ref.AutoIdentifyEPSG() + _name = spatial_ref.GetAuthorityName(None) or spatial_ref.GetAttrValue("AUTHORITY", 0) + _code = ( + spatial_ref.GetAuthorityCode("PROJCS") + or spatial_ref.GetAuthorityCode("GEOGCS") + or spatial_ref.GetAttrValue("AUTHORITY", 1) + ) + return f"{_name}:{_code}" + + def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: + """ + Main function to import the resource. + Internally will call the steps required to import the + data inside the geonode_data database + """ + # for the moment we skip the dyanamic model creation + logger.info("Total number of layers available: 1") + _exec = self._get_execution_request_object(execution_id) + _input = {**_exec.input_params, **{"total_layers": 1}} + orchestrator.update_execution_request_status(execution_id=str(execution_id), input_params=_input) + + try: + filename = Path(files.get("base_file")).stem + # start looping on the layers available + layer_name = self.fixup_name(filename) + + should_be_overwritten = _exec.input_params.get("overwrite_existing_layer") + # should_be_imported check if the user+layername already exists or not + if should_be_imported( + layer_name, + _exec.user, + skip_existing_layer=_exec.input_params.get("skip_existing_layer"), + overwrite_existing_layer=should_be_overwritten, + ): + workspace = DataPublisher(None).workspace + if _exec.input_params.get("resource_pk"): + dataset = Dataset.objects.filter(pk=_exec.input_params.get("resource_pk")).first() + if not dataset: + raise ImportException("The dataset selected for the ovewrite does not exists") + alternate = dataset.alternate.split(":")[-1] + orchestrator.update_execution_request_obj(_exec, {"geonode_resource": dataset}) + else: + user_datasets = Dataset.objects.filter(owner=_exec.user, alternate=f"{workspace.name}:{layer_name}") + + dataset_exists = user_datasets.exists() + + if dataset_exists and should_be_overwritten: + layer_name, alternate = ( + layer_name, + user_datasets.first().alternate.split(":")[-1], + ) + elif not dataset_exists: + alternate = layer_name + else: + alternate = create_alternate(layer_name, execution_id) + + import_orchestrator.apply_async( + ( + files, + execution_id, + str(self), + "geonode.upload.import_resource", + layer_name, + alternate, + exa.IMPORT.value, + ) + ) + return layer_name, alternate, execution_id + + except Exception as e: + logger.error(e) + raise e + return + + def create_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: Dataset = Dataset, + asset=None, + ): + """ + Base function to create the resource into geonode. Each handler can specify + and handle the resource in a different way + """ + saved_dataset = resource_type.objects.filter(alternate__icontains=alternate) + + _exec = self._get_execution_request_object(execution_id) + + workspace = getattr( + settings, + "DEFAULT_WORKSPACE", + getattr(settings, "CASCADE_WORKSPACE", "geonode"), + ) + + _overwrite = _exec.input_params.get("overwrite_existing_layer", False) + # if the layer exists, we just update the information of the dataset by + # let it recreate the catalogue + if not saved_dataset.exists() and _overwrite: + logger.warning( + f"The dataset required {alternate} does not exists, but an overwrite is required, the resource will be created" + ) + + saved_dataset = resource_manager.create( + None, + resource_type=resource_type, + defaults=dict( + name=alternate, + workspace=workspace, + subtype="raster", + alternate=f"{workspace}:{alternate}", + dirty_state=True, + title=layer_name, + owner=_exec.user, + asset=asset, + ), + ) + + saved_dataset.refresh_from_db() + + self.handle_xml_file(saved_dataset, _exec) + self.handle_sld_file(saved_dataset, _exec) + + resource_manager.set_thumbnail(None, instance=saved_dataset) + + ResourceBase.objects.filter(alternate=alternate).update(dirty_state=False) + + saved_dataset.refresh_from_db() + return saved_dataset + + def overwrite_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: Dataset = Dataset, + asset=None, + ): + + _exec = self._get_execution_request_object(execution_id) + + dataset = resource_type.objects.filter(alternate__icontains=alternate, owner=_exec.user) + + _overwrite = _exec.input_params.get("overwrite_existing_layer", False) + # if the layer exists, we just update the information of the dataset by + # let it recreate the catalogue + + if dataset.exists() and _overwrite: + dataset = dataset.first() + + dataset = resource_manager.update(dataset.uuid, instance=dataset) + + self.handle_xml_file(dataset, _exec) + self.handle_sld_file(dataset, _exec) + + resource_manager.set_thumbnail(dataset.uuid, instance=dataset, overwrite=True) + dataset.refresh_from_db() + return dataset + elif not dataset.exists() and _overwrite: + logger.warning( + f"The dataset required {alternate} does not exists, but an overwrite is required, the resource will be created" + ) + return self.create_geonode_resource(layer_name, alternate, execution_id, resource_type, asset) + elif not dataset.exists() and not _overwrite: + logger.warning("The resource does not exists, please use 'create_geonode_resource' to create one") + return + + def handle_xml_file(self, saved_dataset: Dataset, _exec: ExecutionRequest): + _path = _exec.input_params.get("files", {}).get("xml_file", "") + resource_manager.update( + None, + instance=saved_dataset, + xml_file=_path, + metadata_uploaded=True if _path else False, + vals={"dirty_state": True}, + ) + + def handle_sld_file(self, saved_dataset: Dataset, _exec: ExecutionRequest): + _path = _exec.input_params.get("files", {}).get("sld_file", "") + resource_manager.exec( + "set_style", + None, + instance=saved_dataset, + sld_file=_exec.input_params.get("files", {}).get("sld_file", ""), + sld_uploaded=True if _path else False, + vals={"dirty_state": True}, + ) + + def create_resourcehandlerinfo( + self, + handler_module_path: str, + resource: Dataset, + execution_id: ExecutionRequest, + **kwargs, + ): + """ + Create relation between the GeonodeResource and the handler used + to create/copy it + """ + ResourceHandlerInfo.objects.create( + handler_module_path=str(handler_module_path), + resource=resource, + execution_request=execution_id, + kwargs=kwargs.get("kwargs", {}), + ) + + def overwrite_resourcehandlerinfo( + self, + handler_module_path: str, + resource: Dataset, + execution_id: ExecutionRequest, + **kwargs, + ): + """ + Overwrite the ResourceHandlerInfo + """ + if resource.resourcehandlerinfo_set.exists(): + resource.resourcehandlerinfo_set.update( + handler_module_path=handler_module_path, + resource=resource, + execution_request=execution_id, + kwargs=kwargs.get("kwargs", {}) or kwargs, + ) + return + return self.create_resourcehandlerinfo(handler_module_path, resource, execution_id, **kwargs) + + def copy_geonode_resource( + self, + alternate: str, + resource: Dataset, + _exec: ExecutionRequest, + data_to_update: dict, + new_alternate: str, + **kwargs, + ): + resource = self.create_geonode_resource( + layer_name=data_to_update.get("title"), + alternate=new_alternate, + execution_id=str(_exec.exec_id), + asset=kwargs.get("kwargs", {}).get("new_file_location", {}).get("asset", []), + ) + resource.refresh_from_db() + return resource + + def _get_execution_request_object(self, execution_id: str): + return ExecutionRequest.objects.filter(exec_id=execution_id).first() + + @staticmethod + def copy_original_file(dataset): + """ + Copy the original file into a new location + """ + return storage_manager.copy(dataset) + + def _import_resource_rollback(self, exec_id, istance_name=None, *args, **kwargs): + """ + In the raster, this step just generate the alternate, no real action + are done on the database + """ + pass + + def _publish_resource_rollback(self, exec_id, istance_name=None, *args, **kwargs): + """ + We delete the resource from geoserver + """ + logger.info( + f"Rollback publishing step in progress for execid: {exec_id} resource published was: {istance_name}" + ) + exec_object = orchestrator.get_execution_object(exec_id) + handler_module_path = exec_object.input_params.get("handler_module_path") + publisher = DataPublisher(handler_module_path=handler_module_path) + publisher.delete_resource(istance_name) + + +@importer_app.task( + base=ErrorBaseTaskClass, + name="geonode.upload.copy_raster_file", + queue="geonode.upload.copy_raster_file", + max_retries=1, + acks_late=False, + ignore_result=False, + task_track_started=True, +) +def copy_raster_file(exec_id, actual_step, layer_name, alternate, handler_module_path, action, **kwargs): + """ + Perform a copy of the original raster file""" + + original_dataset = ResourceBase.objects.filter(alternate=alternate) + if not original_dataset.exists(): + raise InvalidGeoTiffException("Dataset required does not exists") + + original_dataset = original_dataset.first() + + if not original_dataset.files: + raise InvalidGeoTiffException( + "The original file of the dataset is not available, Is not possible to copy the dataset" + ) + + new_file_location = orchestrator.load_handler(handler_module_path).copy_original_file(original_dataset) + + new_dataset_alternate = create_alternate(original_dataset.title, exec_id) + + additional_kwargs = { + "original_dataset_alternate": original_dataset.alternate, + "new_dataset_alternate": new_dataset_alternate, + "new_file_location": new_file_location, + } + + task_params = ( + {}, + exec_id, + handler_module_path, + actual_step, + layer_name, + new_dataset_alternate, + action, + ) + + import_orchestrator.apply_async(task_params, additional_kwargs) + + return "copy_raster", layer_name, alternate, exec_id diff --git a/geonode/upload/handlers/common/remote.py b/geonode/upload/handlers/common/remote.py new file mode 100755 index 00000000000..6cc827f0e15 --- /dev/null +++ b/geonode/upload/handlers/common/remote.py @@ -0,0 +1,292 @@ +######################################################################### +# +# Copyright (C) 2024 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 +import os + +import requests +from geonode.layers.models import Dataset +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.api.exceptions import ImportException +from geonode.upload.handlers.base import BaseHandler +from geonode.upload.handlers.common.serializer import RemoteResourceSerializer +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload.orchestrator import orchestrator +from geonode.upload.celery_tasks import import_orchestrator +from geonode.upload.handlers.utils import create_alternate +from geonode.upload.utils import ImporterRequestAction as ira +from geonode.base.models import ResourceBase, Link +from urllib.parse import urlparse +from geonode.base.enumerations import SOURCE_TYPE_REMOTE +from geonode.resource.manager import resource_manager +from geonode.resource.models import ExecutionRequest + +logger = logging.getLogger("importer") + + +class BaseRemoteResourceHandler(BaseHandler): + """ + Handler to import remote resources into GeoNode data db + It must provide the task_lists required to comple the upload + As first implementation only remote 3dtiles are supported + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + @staticmethod + def has_serializer(data) -> bool: + if "url" in data: + return RemoteResourceSerializer + return False + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + if "url" in _data: + return True + return False + + @staticmethod + def is_valid_url(url, **kwargs): + """ + We mark it as valid if the urls is reachable + and if the url is valid + """ + try: + r = requests.get(url, timeout=10) + r.raise_for_status() + except requests.exceptions.Timeout: + raise ImportException("Timed out") + except Exception: + raise ImportException("The provided URL is not reachable") + return True + + @staticmethod + def extract_params_from_data(_data, action=None): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + if action == exa.COPY.value: + title = json.loads(_data.get("defaults")) + return {"title": title.pop("title")}, _data + + return { + "source": _data.pop("source", "upload"), + "title": _data.pop("title", None), + "url": _data.pop("url", None), + "type": _data.pop("type", None), + "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), + }, _data + + def pre_validation(self, files, execution_id, **kwargs): + """ + Hook for let the handler prepare the data before the validation. + Maybe a file rename, assign the resource to the execution_id + """ + + def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: + """ + Main function to import the resource. + Internally will call the steps required to import the + data inside the geonode_data database + """ + # for the moment we skip the dyanamic model creation + logger.info("Total number of layers available: 1") + _exec = self._get_execution_request_object(execution_id) + _input = {**_exec.input_params, **{"total_layers": 1}} + orchestrator.update_execution_request_status(execution_id=str(execution_id), input_params=_input) + + try: + params = _exec.input_params.copy() + url = params.get("url") + title = params.get("title", None) or os.path.basename(urlparse(url).path) + + # start looping on the layers available + layer_name = self.fixup_name(title) + + should_be_overwritten = _exec.input_params.get("overwrite_existing_layer") + + payload_alternate = params.get("remote_resource_id", None) + + user_datasets = ResourceBase.objects.filter(owner=_exec.user, alternate=payload_alternate or layer_name) + + dataset_exists = user_datasets.exists() + + layer_name, alternate = self.generate_alternate( + layer_name, + execution_id, + should_be_overwritten, + payload_alternate, + user_datasets, + dataset_exists, + ) + + import_orchestrator.apply_async( + ( + files, + execution_id, + str(self), + "geonode.upload.import_resource", + layer_name, + alternate, + exa.IMPORT.value, + ) + ) + return layer_name, alternate, execution_id + + except Exception as e: + logger.error(e) + raise e + + def generate_alternate( + self, + layer_name, + execution_id, + should_be_overwritten, + payload_alternate, + user_datasets, + dataset_exists, + ): + if dataset_exists and should_be_overwritten: + layer_name, alternate = ( + payload_alternate or layer_name, + user_datasets.first().alternate.split(":")[-1], + ) + elif not dataset_exists: + alternate = payload_alternate or layer_name + else: + alternate = create_alternate(payload_alternate or layer_name, execution_id) + return layer_name, alternate + + def create_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: ResourceBase = ResourceBase, + asset=None, + ): + """ + Creating geonode base resource + We ignore the params, we use the function as a interface to keep the same + importer flow. + We create a standard ResourceBase + """ + _exec = orchestrator.get_execution_object(execution_id) + params = _exec.input_params.copy() + + resource = resource_manager.create( + None, + resource_type=resource_type, + defaults=self.generate_resource_payload(layer_name, alternate, asset, _exec, None, **params), + ) + resource_manager.set_thumbnail(None, instance=resource) + + resource = self.create_link(resource, params, alternate) + ResourceBase.objects.filter(alternate=alternate).update(dirty_state=False) + + return resource + + def create_link(self, resource, params: dict, name): + link = Link( + resource=resource, + extension=params.get("type"), + url=params.get("url"), + link_type="data", + name=name, + ) + link.save() + return resource + + def create_resourcehandlerinfo( + self, + handler_module_path: str, + resource: Dataset, + execution_id: ExecutionRequest, + **kwargs, + ): + """ + Create relation between the GeonodeResource and the handler used + to create/copy it + """ + + ResourceHandlerInfo.objects.create( + handler_module_path=handler_module_path, + resource=resource, + execution_request=execution_id, + kwargs=kwargs.get("kwargs", {}) or kwargs, + ) + + def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspace, **kwargs): + return dict( + subtype=kwargs.get("type"), + sourcetype=SOURCE_TYPE_REMOTE, + alternate=alternate, + dirty_state=True, + title=kwargs.get("title", layer_name), + owner=_exec.user, + ) + + def overwrite_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: Dataset = ResourceBase, + asset=None, + ): + _exec = self._get_execution_request_object(execution_id) + resource = resource_type.objects.filter(alternate__icontains=alternate, owner=_exec.user) + + _overwrite = _exec.input_params.get("overwrite_existing_layer", False) + # if the layer exists, we just update the information of the dataset by + # let it recreate the catalogue + if resource.exists() and _overwrite: + resource = resource.first() + + resource = resource_manager.update(resource.uuid, instance=resource) + resource_manager.set_thumbnail(resource.uuid, instance=resource, overwrite=True) + resource.refresh_from_db() + return resource + elif not resource.exists() and _overwrite: + logger.warning( + f"The dataset required {alternate} does not exists, but an overwrite is required, the resource will be created" + ) + return self.create_geonode_resource(layer_name, alternate, execution_id, resource_type, asset) + elif not resource.exists() and not _overwrite: + logger.warning("The resource does not exists, please use 'create_geonode_resource' to create one") + return diff --git a/geonode/upload/handlers/common/serializer.py b/geonode/upload/handlers/common/serializer.py new file mode 100644 index 00000000000..f1b12b7d8db --- /dev/null +++ b/geonode/upload/handlers/common/serializer.py @@ -0,0 +1,39 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework import serializers +from dynamic_rest.serializers import DynamicModelSerializer +from geonode.base.models import ResourceBase + + +class RemoteResourceSerializer(DynamicModelSerializer): + class Meta: + ref_name = "RemoteResourceSerializer" + model = ResourceBase + view_name = "importer_upload" + fields = ("url", "title", "type", "source", "overwrite_existing_layer") + + url = serializers.URLField(required=True, help_text="URL of the remote service / resource") + title = serializers.CharField(required=True, help_text="Title of the resource. Can be None or Empty") + type = serializers.CharField( + required=True, + help_text="Remote resource type, for example wms or 3dtiles. Is used by the handler to understand if can handle the resource", + ) + source = serializers.CharField(required=False, default="upload") + + overwrite_existing_layer = serializers.BooleanField(required=False, default=False) diff --git a/geonode/upload/handlers/common/test_remote.py b/geonode/upload/handlers/common/test_remote.py new file mode 100644 index 00000000000..89430d618b9 --- /dev/null +++ b/geonode/upload/handlers/common/test_remote.py @@ -0,0 +1,153 @@ +######################################################################### +# +# Copyright (C) 2024 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.test import TestCase +from mock import MagicMock, patch +from geonode.upload.api.exceptions import ImportException +from geonode.upload.handlers.common.remote import BaseRemoteResourceHandler +from django.contrib.auth import get_user_model +from geonode.upload.handlers.common.serializer import RemoteResourceSerializer +from geonode.upload.orchestrator import orchestrator +from geonode.base.populate_test_data import create_single_dataset +from geonode.resource.models import ExecutionRequest +from geonode.base.models import ResourceBase + + +class TestBaseRemoteResourceHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = BaseRemoteResourceHandler() + cls.valid_url = ( + "https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples/main/1.1/TilesetWithFullMetadata/tileset.json" + ) + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = { + "url": "http://abc123defsadsa.org", + "title": "Remote Title", + "type": "3dtiles", + } + cls.valid_files = { + "url": cls.valid_url, + "title": "Remote Title", + "type": "3dtiles", + } + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="stazioni_metropolitana", owner=cls.owner) + + def test_can_handle_should_return_true_for_remote(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.file"}) + self.assertFalse(actual) + + def test_should_get_the_specific_serializer(self): + actual = self.handler.has_serializer(self.valid_files) + self.assertEqual(type(actual), type(RemoteResourceSerializer)) + + def test_create_error_log(self): + """ + Should return the formatted way for the log of the handler + """ + actual = self.handler.create_error_log( + Exception("my exception"), + "foo_task_name", + *["exec_id", "layer_name", "alternate"], + ) + expected = "Task: foo_task_name raised an error during actions for layer: alternate: my exception" + self.assertEqual(expected, actual) + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 3) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_geojson(self): + expected = ( + "start_copy", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 2) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_url_is_invalid(self): + with self.assertRaises(ImportException) as _exc: + self.handler.is_valid_url(url=self.invalid_files["url"]) + + self.assertIsNotNone(_exc) + self.assertTrue("The provided url is not reachable") + + def test_is_valid_should_pass_with_valid_url(self): + self.handler.is_valid_url(url=self.valid_files["url"]) + + def test_extract_params_from_data(self): + actual, _data = self.handler.extract_params_from_data( + _data={"defaults": '{"url": "http://abc123defsadsa.org", "title": "Remote Title", "type": "3dtiles"}'}, + action="import", + ) + self.assertTrue("title" in actual) + self.assertTrue("url" in actual) + self.assertTrue("type" in actual) + + @patch("geonode.upload.handlers.common.remote.import_orchestrator") + def test_import_resource_should_work(self, patch_upload): + patch_upload.apply_async.side_effect = MagicMock() + try: + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params=self.valid_files, + ) + + # start the resource import + self.handler.import_resource(files=self.valid_files, execution_id=str(exec_id)) + patch_upload.apply_async.assert_called_once() + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_create_geonode_resource(self): + exec_id = orchestrator.create_execution_request( + user=self.owner, + func_name="funct1", + step="step", + input_params={ + "url": "http://abc123defsadsa.org", + "title": "Remote Title", + "type": "3dtiles", + }, + ) + + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type=ResourceBase, + asset=None, + ) + self.assertIsNotNone(resource) + self.assertEqual(resource.subtype, "3dtiles") diff --git a/geonode/upload/handlers/common/tests_raster.py b/geonode/upload/handlers/common/tests_raster.py new file mode 100644 index 00000000000..6169e9b0ee5 --- /dev/null +++ b/geonode/upload/handlers/common/tests_raster.py @@ -0,0 +1,95 @@ +######################################################################### +# +# Copyright (C) 2024 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.test import TestCase +from mock import patch +from geonode.upload.handlers.common.raster import BaseRasterFileHandler +from django.contrib.auth import get_user_model +from geonode.upload import project_dir +from geonode.upload.orchestrator import orchestrator +from geonode.base.populate_test_data import create_single_dataset +from geonode.resource.models import ExecutionRequest + + +class TestBaseRasterFileHandler(TestCase): + databases = ("default",) + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = BaseRasterFileHandler() + cls.valid_raster = f"{project_dir}/tests/fixture/test_raster.tif" + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.valid_files = {"base_file": cls.valid_raster} + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="test_grid", owner=cls.owner) + + def test_create_error_log(self): + """ + Should return the formatted way for the log of the handler + """ + actual = self.handler.create_error_log( + Exception("my exception"), + "foo_task_name", + *["exec_id", "layer_name", "alternate"], + ) + expected = "Task: foo_task_name raised an error during actions for layer: alternate: my exception" + self.assertEqual(expected, actual) + + @patch("geonode.upload.handlers.common.vector.BaseVectorFileHandler.get_ogr2ogr_driver") + @patch("geonode.upload.handlers.common.vector.chord") + def test_import_resource_should_not_be_imported(self, celery_chord, ogr2ogr_driver): + """ + If the resource exists and should be skept, the celery task + is not going to be called and the layer is skipped + """ + exec_id = None + try: + # create the executionId + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "skip_existing_layer": True}, + ) + + # start the resource import + self.handler.import_resource(files=self.valid_files, execution_id=str(exec_id)) + + celery_chord.assert_not_called() + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + @patch("geonode.upload.handlers.common.raster.import_orchestrator.apply_async") + def test_import_resource_should_work(self, import_orchestrator): + try: + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={"files": self.valid_files}, + ) + + # start the resource import + self.handler.import_resource(files=self.valid_files, execution_id=str(exec_id)) + + import_orchestrator.assert_called_once() + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() diff --git a/geonode/upload/handlers/common/tests_vector.py b/geonode/upload/handlers/common/tests_vector.py new file mode 100644 index 00000000000..bf99e062f7c --- /dev/null +++ b/geonode/upload/handlers/common/tests_vector.py @@ -0,0 +1,368 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +import shutil +import uuid +from celery.canvas import Signature +from celery import group +from django.conf import settings +from django.test import TestCase +from mock import MagicMock, patch +from geonode.upload.handlers.common.vector import BaseVectorFileHandler, import_with_ogr2ogr +from django.contrib.auth import get_user_model +from geonode.upload import project_dir +from geonode.upload.handlers.gpkg.handler import GPKGFileHandler +from geonode.upload.orchestrator import orchestrator +from geonode.base.populate_test_data import create_single_dataset +from geonode.resource.models import ExecutionRequest +from dynamic_models.models import ModelSchema +from osgeo import ogr +from django.test.utils import override_settings + + +class TestBaseVectorFileHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = BaseVectorFileHandler() + cls.valid_gpkg = f"{project_dir}/tests/fixture/valid.gpkg" + cls.invalid_gpkg = f"{project_dir}/tests/fixture/invalid.gpkg" + cls.no_crs_gpkg = f"{project_dir}/tests/fixture/noCrsTable.gpkg" + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_gpkg} + cls.valid_files = {"base_file": "/tmp/valid.gpkg"} + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="stazioni_metropolitana", owner=cls.owner) + + def setUp(self) -> None: + shutil.copy(self.valid_gpkg, "/tmp") + super().setUp() + + def test_create_error_log(self): + """ + Should return the formatted way for the log of the handler + """ + actual = self.handler.create_error_log( + Exception("my exception"), + "foo_task_name", + *["exec_id", "layer_name", "alternate"], + ) + expected = "Task: foo_task_name raised an error during actions for layer: alternate: my exception" + self.assertEqual(expected, actual) + + def test_create_dynamic_model_fields(self): + try: + # Prepare the test + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "skip_existing_layer": True}, + ) + schema, _ = ModelSchema.objects.get_or_create(name="test_handler", db_name="datastore") + layers = ogr.Open(self.valid_gpkg) + + # starting the tests + dynamic_model, celery_group = self.handler.create_dynamic_model_fields( + layer=[x for x in layers][0], + dynamic_model_schema=schema, + overwrite=False, + execution_id=str(exec_id), + layer_name="stazioni_metropolitana", + ) + + self.assertIsNotNone(dynamic_model) + self.assertIsInstance(celery_group, group) + self.assertEqual(1, len(celery_group.tasks)) + self.assertEqual("geonode.upload.create_dynamic_structure", celery_group.tasks[0].name) + finally: + if schema: + schema.delete() + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_setup_dynamic_model_no_dataset_no_modelschema(self): + self._assert_test_result() + + def test_setup_dynamic_model_no_dataset_no_modelschema_overwrite_true(self): + self._assert_test_result(overwrite=True) + + def test_setup_dynamic_model_with_dataset_no_modelschema_overwrite_false(self): + create_single_dataset(name="stazioni_metropolitana", owner=self.user) + self._assert_test_result(overwrite=False) + + def test_setup_dynamic_model_with_dataset_no_modelschema_overwrite_True(self): + create_single_dataset(name="stazioni_metropolitana", owner=self.user) + self._assert_test_result(overwrite=True) + + def test_setup_dynamic_model_no_dataset_with_modelschema_overwrite_false(self): + ModelSchema.objects.get_or_create(name="stazioni_metropolitana", db_name="datastore") + self._assert_test_result(overwrite=False) + + def test_setup_dynamic_model_with_dataset_with_modelschema_overwrite_false(self): + create_single_dataset(name="stazioni_metropolitana", owner=self.user) + ModelSchema.objects.create(name="stazioni_metropolitana", db_name="datastore", managed=True) + self._assert_test_result(overwrite=False) + + def _assert_test_result(self, overwrite=False): + try: + # Prepare the test + exec_id = orchestrator.create_execution_request( + user=self.user, + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "skip_existing_layer": True}, + ) + + layers = ogr.Open(self.valid_gpkg) + + # starting the tests + dynamic_model, layer_name, celery_group = self.handler.setup_dynamic_model( + layer=[x for x in layers][0], + execution_id=str(exec_id), + should_be_overwritten=overwrite, + username=self.user, + ) + + self.assertIsNotNone(dynamic_model) + + # check if the uuid has been added to the model name + self.assertIsNotNone(layer_name) + + self.assertIsInstance(celery_group, group) + self.assertEqual(1, len(celery_group.tasks)) + self.assertEqual("geonode.upload.create_dynamic_structure", celery_group.tasks[0].name) + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + @patch("geonode.upload.handlers.common.vector.BaseVectorFileHandler.get_ogr2ogr_driver") + @patch("geonode.upload.handlers.common.vector.chord") + def test_import_resource_should_not_be_imported(self, celery_chord, ogr2ogr_driver): + """ + If the resource exists and should be skept, the celery task + is not going to be called and the layer is skipped + """ + exec_id = None + try: + # create the executionId + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "skip_existing_layer": True}, + ) + + with self.assertRaises(Exception) as exception: + # start the resource import + self.handler.import_resource(files=self.valid_files, execution_id=str(exec_id)) + self.assertIn( + "No valid layers found", + exception.exception.args[0], + "No valid layers found.", + ) + + celery_chord.assert_not_called() + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + @patch("geonode.upload.handlers.common.vector.BaseVectorFileHandler.get_ogr2ogr_driver") + @patch("geonode.upload.handlers.common.vector.chord") + def test_import_resource_should_work(self, celery_chord, ogr2ogr_driver): + try: + ogr2ogr_driver.return_value = ogr.GetDriverByName("GPKG") + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={"files": self.valid_files}, + ) + + # start the resource import + self.handler.import_resource(files=self.valid_files, execution_id=str(exec_id)) + + celery_chord.assert_called_once() + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_get_ogr2ogr_task_group(self): + _uuid = uuid.uuid4() + + actual = self.handler.get_ogr2ogr_task_group( + str(_uuid), + files=self.valid_files, + layer="dataset", + should_be_overwritten=True, + alternate="abc", + ) + self.assertIsInstance(actual, (Signature,)) + self.assertEqual("geonode.upload.import_with_ogr2ogr", actual.task) + + @patch("geonode.upload.handlers.common.vector.Popen") + def test_import_with_ogr2ogr_without_errors_should_call_the_right_command(self, _open): + _uuid = uuid.uuid4() + + comm = MagicMock() + comm.communicate.return_value = b"", b"" + _open.return_value = comm + + _task, alternate, execution_id = import_with_ogr2ogr( + execution_id=str(_uuid), + files=self.valid_files, + original_name="dataset", + handler_module_path=str(self.handler), + ovverwrite_layer=False, + alternate="alternate", + ) + + self.assertEqual("ogr2ogr", _task) + self.assertEqual(alternate, "alternate") + self.assertEqual(str(_uuid), execution_id) + + _datastore = settings.DATABASES["datastore"] + _open.assert_called_once() + _open.assert_called_with( + "/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host=" + + os.getenv("DATABASE_HOST", "localhost") + + " port=5432 user='" + + _datastore["USER"] + + "' password='" + + _datastore["PASSWORD"] + + '\' " "' + + self.valid_files.get("base_file") + + '" -nln alternate "dataset"', + stdout=-1, + stderr=-1, + shell=True, # noqa + ) + + @patch("geonode.upload.handlers.common.vector.Popen") + def test_import_with_ogr2ogr_with_errors_should_raise_exception(self, _open): + _uuid = uuid.uuid4() + + comm = MagicMock() + comm.communicate.return_value = b"", b"ERROR: some error here" + _open.return_value = comm + + with self.assertRaises(Exception): + import_with_ogr2ogr( + execution_id=str(_uuid), + files=self.valid_files, + original_name="dataset", + handler_module_path=str(self.handler), + ovverwrite_layer=False, + alternate="alternate", + ) + + _datastore = settings.DATABASES["datastore"] + + _open.assert_called_once() + _open.assert_called_with( + "/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host=" + + os.getenv("DATABASE_HOST", "localhost") + + " port=5432 user='" + + _datastore["USER"] + + "' password='" + + _datastore["PASSWORD"] + + '\' " "' + + self.valid_files.get("base_file") + + '" -nln alternate "dataset"', + stdout=-1, + stderr=-1, + shell=True, # noqa + ) + + @patch.dict(os.environ, {"OGR2OGR_COPY_WITH_DUMP": "True"}, clear=True) + @patch("geonode.upload.handlers.common.vector.Popen") + def test_import_with_ogr2ogr_without_errors_should_call_the_right_command_if_dump_is_enabled(self, _open): + _uuid = uuid.uuid4() + + comm = MagicMock() + comm.communicate.return_value = b"", b"" + _open.return_value = comm + + _task, alternate, execution_id = import_with_ogr2ogr( + execution_id=str(_uuid), + files=self.valid_files, + original_name="dataset", + handler_module_path=str(self.handler), + ovverwrite_layer=False, + alternate="alternate", + ) + + self.assertEqual("ogr2ogr", _task) + self.assertEqual(alternate, "alternate") + self.assertEqual(str(_uuid), execution_id) + + _open.assert_called_once() + _call_as_string = _open.mock_calls[0][1][0] + + self.assertTrue("-f PGDump /vsistdout/" in _call_as_string) + self.assertTrue("psql -d" in _call_as_string) + self.assertFalse("-f PostgreSQL PG" in _call_as_string) + + def test_select_valid_layers(self): + """ + The function should return only the datasets with a geometry + The other one are discarded + """ + all_layers = GPKGFileHandler().get_ogr2ogr_driver().Open(self.no_crs_gpkg) + + with self.assertLogs(level="ERROR") as _log: + valid_layer = GPKGFileHandler()._select_valid_layers(all_layers) + + self.assertIn( + "The following layer layer_styles does not have a Coordinate Reference System (CRS) and will be skipped.", + [x.message for x in _log.records], + ) + self.assertEqual(1, len(valid_layer)) + self.assertEqual("mattia_test", valid_layer[0].GetName()) + + @override_settings(MEDIA_ROOT="/tmp") + def test_perform_last_step(self): + """ + Output params in perform_last_step should return the detail_url and the ID + of the resource created + """ + handler = GPKGFileHandler() + # creating exec_id for the import + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "store_spatial_file": True}, + ) + + # create_geonode_resource + resource = handler.create_geonode_resource( + "layer_name", + "layer_alternate", + str(exec_id), + ) + exec_obj = orchestrator.get_execution_object(str(exec_id)) + handler.create_resourcehandlerinfo(str(handler), resource, exec_obj) + # calling the last_step + handler.perform_last_step(str(exec_id)) + expected_output = {"resources": [{"id": resource.pk, "detail_url": resource.detail_url}]} + exec_obj.refresh_from_db() + self.assertDictEqual(expected_output, exec_obj.output_params) diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py new file mode 100644 index 00000000000..ddf755d55a1 --- /dev/null +++ b/geonode/upload/handlers/common/vector.py @@ -0,0 +1,941 @@ +######################################################################### +# +# Copyright (C) 2024 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 ast +from django.db import connections +from geonode.upload.publisher import DataPublisher +from geonode.upload.utils import call_rollback_function +import json +import logging +import os +from subprocess import PIPE, Popen +from typing import List +from celery import chord, group + +from django.conf import settings +from dynamic_models.models import ModelSchema +from dynamic_models.schema import ModelSchemaEditor +from geonode.base.models import ResourceBase +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.layers.models import Dataset +from geonode.upload.celery_tasks import ErrorBaseTaskClass, create_dynamic_structure +from geonode.upload.handlers.base import BaseHandler +from geonode.upload.handlers.gpkg.tasks import SingleMessageErrorHandler +from geonode.upload.handlers.utils import ( + GEOM_TYPE_MAPPING, + STANDARD_TYPE_MAPPING, + UploadSourcesEnum, + drop_dynamic_model_schema, +) +from geonode.resource.manager import resource_manager +from geonode.resource.models import ExecutionRequest +from osgeo import ogr +from geonode.upload.api.exceptions import ImportException +from geonode.upload.celery_app import importer_app +from geonode.assets.utils import copy_assets_and_links, get_default_asset + +from geonode.upload.handlers.utils import create_alternate, should_be_imported +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload.orchestrator import orchestrator +from django.db.models import Q +import pyproj +from geonode.geoserver.security import delete_dataset_cache, set_geowebcache_invalidate_cache + +logger = logging.getLogger("importer") + + +class BaseVectorFileHandler(BaseHandler): + """ + Handler to import Vector files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + @property + def default_geometry_column_name(self): + return "geometry" + + @property + def supported_file_extension_config(self): + return NotImplementedError + + @staticmethod + def get_geoserver_store_name(default=None): + """ + Method that return the base store name where to save the data in geoserver + and a boolean to know if the store should be created. + For vector, the store must be created + """ + return os.environ.get("GEONODE_GEODATABASE", "geonode_data"), True + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps + """ + result = Popen("ogr2ogr --version", stdout=PIPE, stderr=PIPE, shell=True) + _, stderr = result.communicate() + if stderr: + raise ImportException(stderr) + return True + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + if _data.get("source", None) != UploadSourcesEnum.upload.value: + return False + return True + + @staticmethod + def has_serializer(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + return False + + @staticmethod + def can_do(action) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + return action in BaseHandler.ACTIONS + + @staticmethod + def create_error_log(exc, task_name, *args): + """ + This function will handle the creation of the log error for each message. + This is helpful and needed, so each handler can specify the log as needed + """ + return f"Task: {task_name} raised an error during actions for layer: {args[-1]}: {exc}" + + @staticmethod + def extract_params_from_data(_data, action=None): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + if action == exa.COPY.value: + title = json.loads(_data.get("defaults")) + return {"title": title.pop("title")}, _data + + return { + "skip_existing_layers": _data.pop("skip_existing_layers", "False"), + "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), + "resource_pk": _data.pop("resource_pk", None), + "store_spatial_file": _data.pop("store_spatial_files", "True"), + "source": _data.pop("source", "upload"), + }, _data + + @staticmethod + def publish_resources(resources: List[str], catalog, store, workspace): + """ + Given a list of strings (which rappresent the table on geoserver) + Will publish the resorces on geoserver + """ + for _resource in resources: + try: + catalog.publish_featuretype( + name=_resource.get("name"), + store=store, + native_crs=_resource.get("crs"), + srs=_resource.get("crs"), + jdbc_virtual_table=_resource.get("name"), + ) + except Exception as e: + if f"Resource named {_resource} already exists in store:" in str(e): + logger.error(f"error during publishing: {e}") + continue + logger.error(f"error during publishing: {e}") + raise e + return True + + def pre_validation(self, files, execution_id, **kwargs): + """ + Hook for let the handler prepare the data before the validation. + Maybe a file rename, assign the resource to the execution_id + """ + + def overwrite_geoserver_resource(self, resource, catalog, store, workspace): + """ + We dont need to do anything for now. + The data is replaced via ogr2ogr + """ + self._delete_resource(resource, catalog, workspace) + return self.publish_resources([resource], catalog, store, workspace) + + def _delete_resource(self, resource, catalog, workspace): + res = None + possible_layer_name = [ + resource.get("name"), + resource.get("name").split(":")[-1], + f"{workspace.name}:{resource.get('name')}", + ] + for el in possible_layer_name: + res = catalog.get_resource(el, workspace=workspace) + if res: + break + if res: + catalog.delete(res, purge="all", recurse=True) + + @staticmethod + def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate): + """ + Define the ogr2ogr command to be executed. + This is a default command that is needed to import a vector file + """ + _datastore = settings.DATABASES["datastore"] + + options = "--config PG_USE_COPY YES" + copy_with_dump = ast.literal_eval(os.getenv("OGR2OGR_COPY_WITH_DUMP", "False")) + + if copy_with_dump: + # use PGDump to load the dataset with ogr2ogr + options += " -f PGDump /vsistdout/ " + else: + # default option with postgres copy + options += " -f PostgreSQL PG:\" dbname='%s' host=%s port=%s user='%s' password='%s' \" " % ( + _datastore["NAME"], + _datastore["HOST"], + _datastore.get("PORT", 5432), + _datastore["USER"], + _datastore["PASSWORD"], + ) + options += f'"{files.get("base_file")}"' + " " + + options += f'-nln {alternate} "{original_name}"' + + if ovverwrite_layer: + options += " -overwrite" + + return options + + @staticmethod + def delete_resource(instance): + """ + Base function to delete the resource with all the dependencies (dynamic model) + """ + try: + name = instance.alternate.split(":")[1] + schema = None + if os.getenv("IMPORTER_ENABLE_DYN_MODELS", False): + schema = ModelSchema.objects.filter(name=name).first() + if schema: + """ + We use the schema editor directly, because the model itself is not managed + on creation, but for the delete since we are going to handle, we can use it + """ + _model_editor = ModelSchemaEditor(initial_model=name, db_name=schema.db_name) + _model_editor.drop_table(schema.as_model()) + ModelSchema.objects.filter(name=name).delete() + except Exception as e: + logger.error(f"Error during deletion of Dynamic Model schema: {e.args[0]}") + + @staticmethod + def perform_last_step(execution_id): + """ + Override this method if there is some extra step to perform + before considering the execution as completed. + For example can be used to trigger an email-send to notify + that the execution is completed + """ + _exec = BaseHandler.perform_last_step(execution_id=execution_id) + if _exec and not _exec.input_params.get("store_spatial_file", False): + resources = ResourceHandlerInfo.objects.filter(execution_request=_exec) + # getting all assets list + assets = filter(None, [get_default_asset(x.resource) for x in resources]) + # we need to loop and cancel one by one to activate the signal + # that delete the file from the filesystem + for asset in assets: + asset.delete() + + def extract_resource_to_publish(self, files, action, layer_name, alternate, **kwargs): + if action == exa.COPY.value: + return [ + { + "name": alternate, + "crs": ResourceBase.objects.filter( + Q(alternate__icontains=layer_name) | Q(title__icontains=layer_name) + ) + .first() + .srid, + } + ] + + layers = self.get_ogr2ogr_driver().Open(files.get("base_file")) + if not layers: + return [] + return [ + { + "name": alternate or layer_name, + "crs": self.identify_authority(_l) if _l.GetSpatialRef() else None, + } + for _l in layers + if self.fixup_name(_l.GetName()) == layer_name + ] + + def identify_authority(self, layer): + try: + layer_wkt = layer.GetSpatialRef().ExportToWkt() + _name = "EPSG" + _code = pyproj.CRS(layer_wkt).to_epsg(min_confidence=20) + if _code is None: + layer_proj4 = layer.GetSpatialRef().ExportToProj4() + _code = pyproj.CRS(layer_proj4).to_epsg(min_confidence=20) + if _code is None: + raise Exception("CRS authority code not found, fallback to default behaviour") + except Exception: + spatial_ref = layer.GetSpatialRef() + spatial_ref.AutoIdentifyEPSG() + _name = spatial_ref.GetAuthorityName(None) or spatial_ref.GetAttrValue("AUTHORITY", 0) + _code = ( + spatial_ref.GetAuthorityCode("PROJCS") + or spatial_ref.GetAuthorityCode("GEOGCS") + or spatial_ref.GetAttrValue("AUTHORITY", 1) + ) + return f"{_name}:{_code}" + + def get_ogr2ogr_driver(self): + """ + Should return the Driver object that is used to open the layers via OGR2OGR + """ + return None + + def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: + """ + Main function to import the resource. + Internally will call the steps required to import the + data inside the geonode_data database + """ + all_layers = self.get_ogr2ogr_driver().Open(files.get("base_file")) + layers = self._select_valid_layers(all_layers) + # for the moment we skip the dyanamic model creation + layer_count = len(layers) + logger.info(f"Total number of layers available: {layer_count}") + _exec = self._get_execution_request_object(execution_id) + _input = {**_exec.input_params, **{"total_layers": layer_count}} + orchestrator.update_execution_request_status(execution_id=str(execution_id), input_params=_input) + dynamic_model = None + celery_group = None + try: + if len(layers) == 0: + raise Exception("No valid layers found") + + # start looping on the layers available + + for index, layer in enumerate(layers, start=1): + layer_name = self.fixup_name(layer.GetName()) + + should_be_overwritten = _exec.input_params.get("overwrite_existing_layer") + # should_be_imported check if the user+layername already exists or not + if ( + should_be_imported( + layer_name, + _exec.user, + skip_existing_layer=_exec.input_params.get("skip_existing_layer"), + overwrite_existing_layer=should_be_overwritten, + ) + # and layer.GetGeometryColumn() is not None + ): + # update the execution request object + # setup dynamic model and retrieve the group task needed for tun the async workflow + # create the async task for create the resource into geonode_data with ogr2ogr + if os.getenv("IMPORTER_ENABLE_DYN_MODELS", False): + ( + dynamic_model, + alternate, + celery_group, + ) = self.setup_dynamic_model( + layer, + execution_id, + should_be_overwritten, + username=_exec.user, + ) + else: + alternate = self.find_alternate_by_dataset(_exec, layer_name, should_be_overwritten) + + ogr_res = self.get_ogr2ogr_task_group( + execution_id, + files, + layer.GetName().lower(), + should_be_overwritten, + alternate, + ) + + if os.getenv("IMPORTER_ENABLE_DYN_MODELS", False): + group_to_call = group( + celery_group.set(link_error=["dynamic_model_error_callback"]), + ogr_res.set(link_error=["dynamic_model_error_callback"]), + ) + else: + group_to_call = group( + ogr_res.set(link_error=["dynamic_model_error_callback"]), + ) + + # prepare the async chord workflow with the on_success and on_fail methods + workflow = chord(group_to_call)( # noqa + import_next_step.s( + execution_id, + str(self), # passing the handler module path + "geonode.upload.import_resource", + layer_name, + alternate, + **kwargs, + ) + ) + except Exception as e: + logger.error(e) + if dynamic_model: + """ + In case of fail, we want to delete the dynamic_model schema and his field + to keep the DB in a consistent state + """ + drop_dynamic_model_schema(dynamic_model) + raise e + return + + def _select_valid_layers(self, all_layers): + layers = [] + for layer in all_layers: + try: + self.identify_authority(layer) + layers.append(layer) + except Exception as e: + logger.error(e) + logger.error( + f"The following layer {layer.GetName()} does not have a Coordinate Reference System (CRS) and will be skipped." + ) + pass + return layers + + def find_alternate_by_dataset(self, _exec_obj, layer_name, should_be_overwritten): + if _exec_obj.input_params.get("resource_pk"): + dataset = Dataset.objects.filter(pk=_exec_obj.input_params.get("resource_pk")).first() + if not dataset: + raise ImportException("The dataset selected for the ovewrite does not exists") + alternate = dataset.alternate.split(":") + return alternate[-1] + + workspace = DataPublisher(None).workspace + dataset_available = Dataset.objects.filter(alternate__iexact=f"{workspace.name}:{layer_name}") + + dataset_exists = dataset_available.exists() + + if dataset_exists and should_be_overwritten: + alternate = dataset_available.first().alternate.split(":")[-1] + elif not dataset_exists: + alternate = layer_name + else: + alternate = create_alternate(layer_name, str(_exec_obj.exec_id)) + + return alternate + + def setup_dynamic_model( + self, + layer: ogr.Layer, + execution_id: str, + should_be_overwritten: bool, + username: str, + ): + """ + Extract from the geopackage the layers name and their schema + after the extraction define the dynamic model instances + Returns: + - dynamic_model as model, so the actual dynamic instance + - alternate -> the alternate of the resource which contains (if needed) the uuid + - celery_group -> the celery group of the field creation + """ + + layer_name = self.fixup_name(layer.GetName()) + workspace = DataPublisher(None).workspace + user_datasets = Dataset.objects.filter(owner=username, alternate__iexact=f"{workspace.name}:{layer_name}") + dynamic_schema = ModelSchema.objects.filter(name__iexact=layer_name) + + dynamic_schema_exists = dynamic_schema.exists() + dataset_exists = user_datasets.exists() + + if dataset_exists and dynamic_schema_exists and should_be_overwritten: + """ + If the user have a dataset, the dynamic model has already been created and is in overwrite mode, + we just take the dynamic_model to overwrite the existing one + """ + dynamic_schema = dynamic_schema.get() + elif not dataset_exists and not dynamic_schema_exists: + """ + cames here when is a new brand upload or when (for any reasons) the dataset exists but the + dynamic model has not been created before + """ + # layer_name = create_alternate(layer_name, execution_id) + dynamic_schema = ModelSchema.objects.create( + name=layer_name, + db_name="datastore", + managed=False, + db_table_name=layer_name, + ) + elif ( + (not dataset_exists and dynamic_schema_exists) + or (dataset_exists and dynamic_schema_exists and not should_be_overwritten) + or (dataset_exists and not dynamic_schema_exists) + ): + """ + it comes here when the layer should not be overrided so we append the UUID + to the layer to let it proceed to the next steps + """ + layer_name = create_alternate(layer_name, execution_id) + dynamic_schema, _ = ModelSchema.objects.get_or_create( + name=layer_name, + db_name="datastore", + managed=False, + db_table_name=layer_name, + ) + else: + raise ImportException("Error during the upload of the gpkg file. The dataset does not exists") + + # define standard field mapping from ogr to django + dynamic_model, celery_group = self.create_dynamic_model_fields( + layer=layer, + dynamic_model_schema=dynamic_schema, + overwrite=should_be_overwritten, + execution_id=execution_id, + layer_name=layer_name, + ) + return dynamic_model, layer_name, celery_group + + def create_dynamic_model_fields( + self, + layer: str, + dynamic_model_schema: ModelSchema, + overwrite: bool, + execution_id: str, + layer_name: str, + ): + # retrieving the field schema from ogr2ogr and converting the type to Django Types + layer_schema = [{"name": x.name.lower(), "class_name": self._get_type(x), "null": True} for x in layer.schema] + if ( + layer.GetGeometryColumn() + or self.default_geometry_column_name + and ogr.GeometryTypeToName(layer.GetGeomType()) not in ["Geometry Collection", "Unknown (any)", "None"] + ): + # the geometry colum is not returned rom the layer.schema, so we need to extract it manually + layer_schema += [ + { + "name": layer.GetGeometryColumn() or self.default_geometry_column_name, + "class_name": GEOM_TYPE_MAPPING.get( + self.promote_to_multi(ogr.GeometryTypeToName(layer.GetGeomType())) + ), + "dim": (2 if not ogr.GeometryTypeToName(layer.GetGeomType()).lower().startswith("3d") else 3), + } + ] + + # ones we have the schema, here we create a list of chunked value + # so the async task will handle max of 30 field per task + list_chunked = [layer_schema[i : i + 30] for i in range(0, len(layer_schema), 30)] # noqa + + # definition of the celery group needed to run the async workflow. + # in this way each task of the group will handle only 30 field + celery_group = group( + create_dynamic_structure.s(execution_id, schema, dynamic_model_schema.id, overwrite, layer_name) + for schema in list_chunked + ) + + return dynamic_model_schema, celery_group + + def promote_to_multi(self, geometry_name: str): + """ + If needed change the name of the geometry, by promoting it to Multi + example if is Point -> MultiPoint + Needed for the shapefiles + """ + return geometry_name + + def create_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: Dataset = Dataset, + asset=None, + ): + """ + Base function to create the resource into geonode. Each handler can specify + and handle the resource in a different way + """ + saved_dataset = resource_type.objects.filter(alternate__icontains=alternate) + + _exec = self._get_execution_request_object(execution_id) + + workspace = getattr( + settings, + "DEFAULT_WORKSPACE", + getattr(settings, "CASCADE_WORKSPACE", "geonode"), + ) + + _overwrite = _exec.input_params.get("overwrite_existing_layer", False) + # if the layer exists, we just update the information of the dataset by + # let it recreate the catalogue + if not saved_dataset.exists() and _overwrite: + logger.warning( + f"The dataset required {alternate} does not exists, but an overwrite is required, the resource will be created" + ) + + saved_dataset = resource_manager.create( + None, + resource_type=resource_type, + defaults=self.generate_resource_payload(layer_name, alternate, asset, _exec, workspace), + ) + + saved_dataset.refresh_from_db() + + self.handle_xml_file(saved_dataset, _exec) + self.handle_sld_file(saved_dataset, _exec) + + resource_manager.set_thumbnail(None, instance=saved_dataset) + + ResourceBase.objects.filter(alternate=alternate).update(dirty_state=False) + + saved_dataset.refresh_from_db() + return saved_dataset + + def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspace): + return dict( + name=alternate, + workspace=workspace, + store=os.environ.get("GEONODE_GEODATABASE", "geonode_data"), + subtype="vector", + alternate=f"{workspace}:{alternate}", + dirty_state=True, + title=layer_name, + owner=_exec.user, + asset=asset, + ) + + def overwrite_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: Dataset = Dataset, + asset=None, + ): + _exec = self._get_execution_request_object(execution_id) + + dataset = resource_type.objects.filter(pk=_exec.input_params.get("resource_pk"), owner=_exec.user) + + _overwrite = _exec.input_params.get("overwrite_existing_layer", False) + # if the layer exists, we just update the information of the dataset by + # let it recreate the catalogue + if dataset.exists() and _overwrite: + dataset = dataset.first() + + delete_dataset_cache(dataset.alternate) + # recalculate featuretype info + DataPublisher(str(self)).recalculate_geoserver_featuretype(dataset) + set_geowebcache_invalidate_cache(dataset_alternate=dataset.alternate) + + dataset = resource_manager.update(dataset.uuid, instance=dataset, files=asset.location) + + self.handle_xml_file(dataset, _exec) + self.handle_sld_file(dataset, _exec) + + resource_manager.set_thumbnail(dataset.uuid, instance=dataset, overwrite=True) + dataset.refresh_from_db() + return dataset + elif not dataset.exists() and _overwrite: + logger.warning( + f"The dataset required {alternate} does not exists, but an overwrite is required, the resource will be created" + ) + return self.create_geonode_resource(layer_name, alternate, execution_id, resource_type, asset) + elif not dataset.exists() and not _overwrite: + logger.warning("The resource does not exists, please use 'create_geonode_resource' to create one") + return + + def handle_xml_file(self, saved_dataset: Dataset, _exec: ExecutionRequest): + _path = _exec.input_params.get("files", {}).get("xml_file", "") + resource_manager.update( + None, + instance=saved_dataset, + xml_file=_path, + metadata_uploaded=True if _path else False, + vals={"dirty_state": True}, + ) + + def handle_sld_file(self, saved_dataset: Dataset, _exec: ExecutionRequest): + _path = _exec.input_params.get("files", {}).get("sld_file", "") + resource_manager.exec( + "set_style", + None, + instance=saved_dataset, + sld_file=_exec.input_params.get("files", {}).get("sld_file", ""), + sld_uploaded=True if _path else False, + vals={"dirty_state": True}, + ) + + def create_resourcehandlerinfo( + self, + handler_module_path: str, + resource: Dataset, + execution_id: ExecutionRequest, + **kwargs, + ): + """ + Create relation between the GeonodeResource and the handler used + to create/copy it + """ + ResourceHandlerInfo.objects.create( + handler_module_path=handler_module_path, + resource=resource, + execution_request=execution_id, + kwargs=kwargs.get("kwargs", {}) or kwargs, + ) + + def overwrite_resourcehandlerinfo( + self, + handler_module_path: str, + resource: Dataset, + execution_id: ExecutionRequest, + **kwargs, + ): + """ + Overwrite the ResourceHandlerInfo + """ + if resource.resourcehandlerinfo_set.exists(): + resource.resourcehandlerinfo_set.update( + handler_module_path=handler_module_path, + resource=resource, + execution_request=execution_id, + kwargs=kwargs.get("kwargs", {}) or kwargs, + ) + return + return self.create_resourcehandlerinfo(handler_module_path, resource, execution_id, **kwargs) + + def copy_geonode_resource( + self, + alternate: str, + resource: Dataset, + _exec: ExecutionRequest, + data_to_update: dict, + new_alternate: str, + **kwargs, + ): + + new_resource = self.create_geonode_resource( + layer_name=data_to_update.get("title"), + alternate=new_alternate, + execution_id=str(_exec.exec_id), + asset=get_default_asset(resource), + ) + copy_assets_and_links(resource, target=new_resource) + new_resource.refresh_from_db() + return new_resource + + def get_ogr2ogr_task_group( + self, + execution_id: str, + files: dict, + layer, + should_be_overwritten: bool, + alternate: str, + ): + """ + In case the OGR2OGR is different from the default one, is enough to ovverride this method + and return the celery task object needed + """ + handler_module_path = str(self) + return import_with_ogr2ogr.s( + execution_id, + files, + layer.lower(), + handler_module_path, + should_be_overwritten, + alternate, + ) + + def _get_execution_request_object(self, execution_id: str): + return ExecutionRequest.objects.filter(exec_id=execution_id).first() + + def _get_type(self, _type: str): + """ + Used to get the standard field type in the dynamic_model_field definition + """ + return STANDARD_TYPE_MAPPING.get(ogr.FieldDefn.GetTypeName(_type)) + + def _import_resource_rollback(self, exec_id, instance_name=None, *args, **kwargs): + """ + We use the schema editor directly, because the model itself is not managed + on creation, but for the delete since we are going to handle, we can use it + """ + logger.info( + f"Rollback dynamic model & ogr2ogr step in progress for execid: {exec_id} resource published was: {instance_name}" + ) + schema = None + if os.getenv("IMPORTER_ENABLE_DYN_MODELS", False): + schema = ModelSchema.objects.filter(name=instance_name).first() + if schema is not None: + _model_editor = ModelSchemaEditor(initial_model=instance_name, db_name=schema.db_name) + _model_editor.drop_table(schema.as_model()) + ModelSchema.objects.filter(name=instance_name).delete() + elif schema is None: + try: + logger.warning("Dynamic model does not exists, removing ogr2ogr table in progress") + if instance_name is None: + logger.warning("No table created, skipping...") + return + db_name = os.getenv("DEFAULT_BACKEND_DATASTORE", "datastore") + with connections[db_name].cursor() as cursor: + cursor.execute(f"DROP TABLE {instance_name}") + except Exception as e: + logger.warning(e) + pass + + def _publish_resource_rollback(self, exec_id, instance_name=None, *args, **kwargs): + """ + We delete the resource from geoserver + """ + logger.info( + f"Rollback publishing step in progress for execid: {exec_id} resource published was: {instance_name}" + ) + exec_object = orchestrator.get_execution_object(exec_id) + handler_module_path = exec_object.input_params.get("handler_module_path") + publisher = DataPublisher(handler_module_path=handler_module_path) + publisher.delete_resource(instance_name) + + +@importer_app.task( + base=ErrorBaseTaskClass, + name="geonode.upload.import_next_step", + queue="geonode.upload.import_next_step", + task_track_started=True, +) +def import_next_step( + _, + execution_id: str, + handlers_module_path: str, + actual_step: str, + layer_name: str, + alternate: str, + **kwargs: dict, +): + """ + If the ingestion of the resource is successfuly, the next step for the layer is called + """ + from geonode.upload.celery_tasks import import_orchestrator + + try: + _exec = orchestrator.get_execution_object(execution_id) + + _files = _exec.input_params.get("files") + # at the end recall the import_orchestrator for the next step + + task_params = ( + _files, + execution_id, + handlers_module_path, + actual_step, + layer_name, + alternate, + exa.IMPORT.value, + ) + + import_orchestrator.apply_async(task_params, kwargs) + except Exception as e: + call_rollback_function( + execution_id, + handlers_module_path=handlers_module_path, + prev_action=exa.IMPORT.value, + layer=layer_name, + alternate=alternate, + error=e, + **kwargs, + ) + + finally: + return "import_next_step", alternate, execution_id + + +@importer_app.task( + base=SingleMessageErrorHandler, + name="geonode.upload.import_with_ogr2ogr", + queue="geonode.upload.import_with_ogr2ogr", + max_retries=1, + acks_late=False, + ignore_result=False, + task_track_started=True, +) +def import_with_ogr2ogr( + execution_id: str, + files: dict, + original_name: str, + handler_module_path: str, + ovverwrite_layer=False, + alternate=None, +): + """ + Perform the ogr2ogr command to import he gpkg inside geonode_data + If the layer should be overwritten, the option is appended dynamically + """ + try: + ogr_exe = "/usr/bin/ogr2ogr" + + options = orchestrator.load_handler(handler_module_path).create_ogr2ogr_command( + files, original_name, ovverwrite_layer, alternate + ) + _datastore = settings.DATABASES["datastore"] + + copy_with_dump = ast.literal_eval(os.getenv("OGR2OGR_COPY_WITH_DUMP", "False")) + + if copy_with_dump: + options += f" | PGPASSWORD={_datastore['PASSWORD']} psql -d {_datastore['NAME']} -h {_datastore['HOST']} -p {_datastore.get('PORT', 5432)} -U {_datastore['USER']} -f -" + + commands = [ogr_exe] + options.split(" ") + + process = Popen(" ".join(commands), stdout=PIPE, stderr=PIPE, shell=True) + stdout, stderr = process.communicate() + if ( + stderr is not None + and stderr != b"" + and b"ERROR" in stderr + and b"error" in stderr + or b"Syntax error" in stderr + ): + try: + err = stderr.decode() + except Exception: + err = stderr.decode("latin1") + logger.error(f"Original error returned: {err}") + message = normalize_ogr2ogr_error(err, original_name) + raise Exception(f"{message} for layer {alternate}") + return "ogr2ogr", alternate, execution_id + except Exception as e: + call_rollback_function( + execution_id, + handlers_module_path=handler_module_path, + prev_action=exa.IMPORT.value, + layer=original_name, + alternate=alternate, + error=e, + **{}, + ) + raise Exception(e) + + +def normalize_ogr2ogr_error(err, original_name): + getting_errors = [y for y in err.split("\n") if "ERROR " in y] + return ", ".join([x.split(original_name)[0] for x in getting_errors if "ERROR" in x]) diff --git a/geonode/upload/handlers/csv/__init__.py b/geonode/upload/handlers/csv/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/csv/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/csv/exceptions.py b/geonode/upload/handlers/csv/exceptions.py new file mode 100644 index 00000000000..691939b4d75 --- /dev/null +++ b/geonode/upload/handlers/csv/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class InvalidCSVException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The csv provided is invalid" + default_code = "invalid_csv" + category = "importer" diff --git a/geonode/upload/handlers/csv/handler.py b/geonode/upload/handlers/csv/handler.py new file mode 100644 index 00000000000..1e3483a25a8 --- /dev/null +++ b/geonode/upload/handlers/csv/handler.py @@ -0,0 +1,233 @@ +######################################################################### +# +# Copyright (C) 2024 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 geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.upload.utils import UploadLimitValidator +from geonode.upload.celery_tasks import create_dynamic_structure +from geonode.upload.handlers.csv.exceptions import InvalidCSVException +from osgeo import ogr +from celery import group +from geonode.base.models import ResourceBase +from dynamic_models.models import ModelSchema +from geonode.upload.handlers.common.vector import BaseVectorFileHandler +from geonode.upload.handlers.utils import GEOM_TYPE_MAPPING +from geonode.upload.utils import ImporterRequestAction as ira + +logger = logging.getLogger("importer") + + +class CSVFileHandler(BaseVectorFileHandler): + """ + Handler to import CSV files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + possible_geometry_column_name = ["geom", "geometry", "wkt_geom", "the_geom"] + possible_lat_column = ["latitude", "lat", "y"] + possible_long_column = ["longitude", "long", "x"] + possible_latlong_column = possible_lat_column + possible_long_column + + @property + def supported_file_extension_config(self): + return { + "id": "csv", + "label": "CSV", + "format": "vector", + "mimeType": ["text/csv"], + "ext": ["csv"], + "optional": ["sld", "xml"], + } + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + base = _data.get("base_file") + if not base: + return False + return ( + base.lower().endswith(".csv") if isinstance(base, str) else base.name.lower().endswith(".csv") + ) and BaseVectorFileHandler.can_handle(_data) + + @staticmethod + def is_valid(files, user, **kwargs): + BaseVectorFileHandler.is_valid(files, user) + # getting the upload limit validation + upload_validator = UploadLimitValidator(user) + upload_validator.validate_parallelism_limit_per_user() + actual_upload = upload_validator._get_parallel_uploads_count() + max_upload = upload_validator._get_max_parallel_uploads() + + layers = CSVFileHandler().get_ogr2ogr_driver().Open(files.get("base_file")) + + if not layers: + raise InvalidCSVException("The CSV provided is invalid, no layers found") + + layers_count = len(layers) + + if layers_count >= max_upload: + raise UploadParallelismLimitException( + detail=f"The number of layers in the CSV {layers_count} is greater than " + f"the max parallel upload permitted: {max_upload} " + f"please upload a smaller file" + ) + elif layers_count + actual_upload >= max_upload: + raise UploadParallelismLimitException( + detail=f"With the provided CSV, the number of max parallel upload will exceed the limit of {max_upload}" + ) + + schema_keys = [x.name.lower() for layer in layers for x in layer.schema] + geom_is_in_schema = any(x in schema_keys for x in CSVFileHandler().possible_geometry_column_name) + has_lat = any(x in CSVFileHandler().possible_lat_column for x in schema_keys) + has_long = any(x in CSVFileHandler().possible_long_column for x in schema_keys) + + fields = CSVFileHandler().possible_geometry_column_name + CSVFileHandler().possible_latlong_column + if has_lat and not has_long: + raise InvalidCSVException( + f"Longitude is missing. Supported names: {', '.join(CSVFileHandler().possible_long_column)}" + ) + + if not has_lat and has_long: + raise InvalidCSVException( + f"Latitude is missing. Supported names: {', '.join(CSVFileHandler().possible_lat_column)}" + ) + + if not geom_is_in_schema and not has_lat and not has_long: + raise InvalidCSVException(f"Not enough geometry field are set. The possibilities are: {','.join(fields)}") + + return True + + def get_ogr2ogr_driver(self): + return ogr.GetDriverByName("CSV") + + @staticmethod + def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate): + """ + Define the ogr2ogr command to be executed. + This is a default command that is needed to import a vector file + """ + base_command = BaseVectorFileHandler.create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate) + additional_option = ' -oo "GEOM_POSSIBLE_NAMES=geom*,the_geom*,wkt_geom" -oo "X_POSSIBLE_NAMES=x,long*" -oo "Y_POSSIBLE_NAMES=y,lat*"' + return ( + f"{base_command } -oo KEEP_GEOM_COLUMNS=NO -lco GEOMETRY_NAME={BaseVectorFileHandler().default_geometry_column_name} " + + additional_option + ) + + def create_dynamic_model_fields( + self, + layer: str, + dynamic_model_schema: ModelSchema, + overwrite: bool, + execution_id: str, + layer_name: str, + ): + # retrieving the field schema from ogr2ogr and converting the type to Django Types + layer_schema = [{"name": x.name.lower(), "class_name": self._get_type(x), "null": True} for x in layer.schema] + if ( + layer.GetGeometryColumn() + or self.default_geometry_column_name + and ogr.GeometryTypeToName(layer.GetGeomType()) not in ["Geometry Collection", "Unknown (any)"] + ): + # the geometry colum is not returned rom the layer.schema, so we need to extract it manually + # checking if the geometry has been wrogly read as string + schema_keys = [x["name"] for x in layer_schema] + geom_is_in_schema = (x in schema_keys for x in self.possible_geometry_column_name) + if any(geom_is_in_schema) and layer.GetGeomType() == 100: # 100 means None so Geometry not found + field_name = [x for x in self.possible_geometry_column_name if x in schema_keys][0] + index = layer.GetFeature(1).keys().index(field_name) + geom = [x for x in layer.GetFeature(1)][index] + class_name = GEOM_TYPE_MAPPING.get(self.promote_to_multi(geom.split("(")[0].replace(" ", "").title())) + layer_schema = [x for x in layer_schema if field_name not in x["name"]] + elif any(x in self.possible_latlong_column for x in schema_keys): + class_name = GEOM_TYPE_MAPPING.get(self.promote_to_multi("Point")) + else: + class_name = GEOM_TYPE_MAPPING.get(self.promote_to_multi(ogr.GeometryTypeToName(layer.GetGeomType()))) + + layer_schema += [ + { + "name": layer.GetGeometryColumn() or self.default_geometry_column_name, + "class_name": class_name, + "dim": (2 if not ogr.GeometryTypeToName(layer.GetGeomType()).lower().startswith("3d") else 3), + } + ] + + # ones we have the schema, here we create a list of chunked value + # so the async task will handle max of 30 field per task + list_chunked = [layer_schema[i : i + 30] for i in range(0, len(layer_schema), 30)] # noqa + + # definition of the celery group needed to run the async workflow. + # in this way each task of the group will handle only 30 field + celery_group = group( + create_dynamic_structure.s(execution_id, schema, dynamic_model_schema.id, overwrite, layer_name) + for schema in list_chunked + ) + + return dynamic_model_schema, celery_group + + def extract_resource_to_publish(self, files, action, layer_name, alternate, **kwargs): + if action == exa.COPY.value: + return [ + { + "name": alternate, + "crs": ResourceBase.objects.filter(alternate__istartswith=layer_name).first().srid, + } + ] + + layers = self.get_ogr2ogr_driver().Open(files.get("base_file"), 0) + if not layers: + return [] + return [ + { + "name": alternate or layer_name, + "crs": (self.identify_authority(_l)), + } + for _l in layers + if self.fixup_name(_l.GetName()) == layer_name + ] + + def identify_authority(self, layer): + try: + authority_code = super().identify_authority(layer=layer) + return authority_code + except Exception: + return "EPSG:4326" diff --git a/geonode/upload/handlers/csv/tests.py b/geonode/upload/handlers/csv/tests.py new file mode 100644 index 00000000000..dc75996bef4 --- /dev/null +++ b/geonode/upload/handlers/csv/tests.py @@ -0,0 +1,182 @@ +######################################################################### +# +# Copyright (C) 2024 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 uuid +from unittest.mock import MagicMock, patch +import os +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import TestCase +from geonode.base.populate_test_data import create_single_dataset +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.upload.models import UploadParallelismLimit +from geonode.upload import project_dir +from geonode.upload.handlers.common.vector import import_with_ogr2ogr +from geonode.upload.handlers.csv.exceptions import InvalidCSVException +from geonode.upload.handlers.csv.handler import CSVFileHandler +from osgeo import ogr + + +class TestCSVHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = CSVFileHandler() + cls.valid_csv = f"{project_dir}/tests/fixture/valid.csv" + cls.invalid_csv = f"{project_dir}/tests/fixture/invalid.csv" + cls.missing_lat = f"{project_dir}/tests/fixture/missing_lat.csv" + cls.missing_long = f"{project_dir}/tests/fixture/missing_long.csv" + cls.missing_geom = f"{project_dir}/tests/fixture/missing_geom.csv" + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_csv} + cls.valid_files = {"base_file": cls.valid_csv, "source": "upload"} + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="test", owner=cls.owner) + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 4) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_geojson(self): + expected = ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_csv_is_invalid(self): + with self.assertRaises(InvalidCSVException) as _exc: + self.handler.is_valid(files=self.invalid_files, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("The CSV provided is invalid, no layers found" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_csv_missing_geom(self): + with self.assertRaises(InvalidCSVException) as _exc: + self.handler.is_valid(files={"base_file": self.missing_geom}, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("Not enough geometry field are set" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_csv_missing_lat(self): + with self.assertRaises(InvalidCSVException) as _exc: + self.handler.is_valid(files={"base_file": self.missing_lat}, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("Latitude is missing" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_csv_missing_long(self): + with self.assertRaises(InvalidCSVException) as _exc: + self.handler.is_valid(files={"base_file": self.missing_long}, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("Longitude is missing" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): + parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=0) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_files, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_raise_exception_if_layer_are_greater_than_max_parallel_upload( + self, + ): + parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=1) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_files, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_pass_with_valid_csv(self): + self.handler.is_valid(files=self.valid_files, user=self.user) + + def test_get_ogr2ogr_driver_should_return_the_expected_driver(self): + expected = ogr.GetDriverByName("CSV") + actual = self.handler.get_ogr2ogr_driver() + self.assertEqual(type(expected), type(actual)) + + def test_can_handle_should_return_true_for_csv(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.file"}) + self.assertFalse(actual) + + @patch("geonode.upload.handlers.common.vector.Popen") + def test_import_with_ogr2ogr_without_errors_should_call_the_right_command(self, _open): + _uuid = uuid.uuid4() + + comm = MagicMock() + comm.communicate.return_value = b"", b"" + _open.return_value = comm + + _task, alternate, execution_id = import_with_ogr2ogr( + execution_id=str(_uuid), + files=self.valid_files, + original_name="dataset", + handler_module_path=str(self.handler), + ovverwrite_layer=False, + alternate="alternate", + ) + + self.assertEqual("ogr2ogr", _task) + self.assertEqual(alternate, "alternate") + self.assertEqual(str(_uuid), execution_id) + + _datastore = settings.DATABASES["datastore"] + _open.assert_called_once() + _open.assert_called_with( + "/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host=" + + os.getenv("DATABASE_HOST", "localhost") + + " port=5432 user='" + + _datastore["USER"] + + "' password='" + + _datastore["PASSWORD"] + + '\' " "' + + self.valid_csv + + '" -nln alternate "dataset" -oo KEEP_GEOM_COLUMNS=NO -lco GEOMETRY_NAME=geometry -oo "GEOM_POSSIBLE_NAMES=geom*,the_geom*,wkt_geom" -oo "X_POSSIBLE_NAMES=x,long*" -oo "Y_POSSIBLE_NAMES=y,lat*"', # noqa + stdout=-1, + stderr=-1, + shell=True, # noqa + ) diff --git a/geonode/upload/handlers/geojson/__init__.py b/geonode/upload/handlers/geojson/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/geojson/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/geojson/exceptions.py b/geonode/upload/handlers/geojson/exceptions.py new file mode 100644 index 00000000000..333939a91e8 --- /dev/null +++ b/geonode/upload/handlers/geojson/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class InvalidGeoJsonException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The geojson provided is invalid" + default_code = "invalid_geojson" + category = "importer" diff --git a/geonode/upload/handlers/geojson/handler.py b/geonode/upload/handlers/geojson/handler.py new file mode 100644 index 00000000000..6060be709bf --- /dev/null +++ b/geonode/upload/handlers/geojson/handler.py @@ -0,0 +1,139 @@ +######################################################################### +# +# Copyright (C) 2024 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 +import os +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.utils import UploadLimitValidator +from geonode.upload.handlers.common.vector import BaseVectorFileHandler +from osgeo import ogr +from geonode.upload.utils import ImporterRequestAction as ira + +from geonode.upload.handlers.geojson.exceptions import InvalidGeoJsonException + +logger = logging.getLogger("importer") + + +class GeoJsonFileHandler(BaseVectorFileHandler): + """ + Handler to import GeoJson files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + @property + def supported_file_extension_config(self): + return { + "id": "geojson", + "label": "GeoJSON", + "format": "vector", + "ext": ["json", "geojson"], + "optional": ["xml", "sld"], + } + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + base = _data.get("base_file") + if not base: + return False + ext = base.split(".")[-1] if isinstance(base, str) else base.name.split(".")[-1] + if ext in ["json", "geojson"] and BaseVectorFileHandler.can_handle(_data): + """ + Check if is a real geojson based on specification + https://datatracker.ietf.org/doc/html/rfc7946#section-1.4 + """ + try: + _file = base + if isinstance(base, str): + with open(base, "r") as f: + _file = json.loads(f.read()) + else: + _file = json.loads(base.read()) + + return _file.get("type", None) in ["FeatureCollection", "Feature"] + + except Exception: + return False + return False + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps: + """ + # calling base validation checks + BaseVectorFileHandler.is_valid(files, user) + # getting the upload limit validation + upload_validator = UploadLimitValidator(user) + upload_validator.validate_parallelism_limit_per_user() + + _file = files.get("base_file") + if not _file: + raise InvalidGeoJsonException("base file is not provided") + + filename = os.path.basename(_file) + + if len(filename.split(".")) > 2: + # means that there is a dot other than the one needed for the extension + # if we keep it ogr2ogr raise an error, better to remove it + raise InvalidGeoJsonException("Please remove the additional dots in the filename") + + try: + with open(_file, "r") as _readed_file: + json.loads(_readed_file.read()) + except Exception: + raise InvalidGeoJsonException("The provided GeoJson is not valid") + + return True + + def get_ogr2ogr_driver(self): + return ogr.GetDriverByName("GeoJSON") + + @staticmethod + def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate): + """ + Define the ogr2ogr command to be executed. + This is a default command that is needed to import a vector file + """ + + base_command = BaseVectorFileHandler.create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate) + return f"{base_command } -lco GEOMETRY_NAME={BaseVectorFileHandler().default_geometry_column_name}" diff --git a/geonode/upload/handlers/geojson/tests.py b/geonode/upload/handlers/geojson/tests.py new file mode 100644 index 00000000000..acf5913e74e --- /dev/null +++ b/geonode/upload/handlers/geojson/tests.py @@ -0,0 +1,151 @@ +######################################################################### +# +# Copyright (C) 2024 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 uuid +import os +from django.conf import settings +from django.test import TestCase +from mock import MagicMock, patch +from geonode.upload.handlers.common.vector import import_with_ogr2ogr +from geonode.upload.handlers.geojson.exceptions import InvalidGeoJsonException +from geonode.upload.handlers.geojson.handler import GeoJsonFileHandler +from django.contrib.auth import get_user_model +from geonode.upload import project_dir +from geonode.upload.models import UploadParallelismLimit +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.base.populate_test_data import create_single_dataset +from osgeo import ogr + + +class TestGeoJsonFileHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = GeoJsonFileHandler() + cls.valid_geojson = f"{project_dir}/tests/fixture/valid.geojson" + cls.invalid_geojson = f"{project_dir}/tests/fixture/invalid.geojson" + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_geojson} + cls.valid_files = {"base_file": cls.valid_geojson, "source": "upload"} + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="stazioni_metropolitana", owner=cls.owner) + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 4) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_copy(self): + expected = ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): + parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=0) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_files, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_pass_with_valid_geojson(self): + self.handler.is_valid(files=self.valid_files, user=self.user) + + def test_is_valid_should_raise_exception_if_the_geojson_is_invalid(self): + data = {"base_file": "/using/double/dot/in/the/name/is/an/error/file.invalid.geojson"} + with self.assertRaises(InvalidGeoJsonException) as _exc: + self.handler.is_valid(files=data, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("Please remove the additional dots in the filename" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_geojson_is_invalid_format(self): + with self.assertRaises(InvalidGeoJsonException) as _exc: + self.handler.is_valid(files=self.invalid_files, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("The provided GeoJson is not valid" in str(_exc.exception.detail)) + + def test_get_ogr2ogr_driver_should_return_the_expected_driver(self): + expected = ogr.GetDriverByName("GEOJSON") + actual = self.handler.get_ogr2ogr_driver() + self.assertEqual(type(expected), type(actual)) + + def test_can_handle_should_return_true_for_geojson(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.gpkg"}) + self.assertFalse(actual) + + @patch("geonode.upload.handlers.common.vector.Popen") + def test_import_with_ogr2ogr_without_errors_should_call_the_right_command(self, _open): + _uuid = uuid.uuid4() + + comm = MagicMock() + comm.communicate.return_value = b"", b"" + _open.return_value = comm + + _task, alternate, execution_id = import_with_ogr2ogr( + execution_id=str(_uuid), + files=self.valid_files, + original_name="dataset", + handler_module_path=str(self.handler), + ovverwrite_layer=False, + alternate="alternate", + ) + + self.assertEqual("ogr2ogr", _task) + self.assertEqual(alternate, "alternate") + self.assertEqual(str(_uuid), execution_id) + + _datastore = settings.DATABASES["datastore"] + _open.assert_called_once() + _open.assert_called_with( + "/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host=" + + os.getenv("DATABASE_HOST", "localhost") + + " port=5432 user='" + + _datastore["USER"] + + "' password='" + + _datastore["PASSWORD"] + + '\' " "' + + self.valid_files.get("base_file") + + '" -nln alternate "dataset" -lco GEOMETRY_NAME=geometry', + stdout=-1, + stderr=-1, + shell=True, # noqa + ) diff --git a/geonode/upload/handlers/geotiff/__init__.py b/geonode/upload/handlers/geotiff/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/geotiff/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/geotiff/exceptions.py b/geonode/upload/handlers/geotiff/exceptions.py new file mode 100644 index 00000000000..70ae3a38be7 --- /dev/null +++ b/geonode/upload/handlers/geotiff/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class InvalidGeoTiffException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The tiff provided is invalid" + default_code = "invalid_tiff" + category = "importer" diff --git a/geonode/upload/handlers/geotiff/handler.py b/geonode/upload/handlers/geotiff/handler.py new file mode 100644 index 00000000000..e49584a9639 --- /dev/null +++ b/geonode/upload/handlers/geotiff/handler.py @@ -0,0 +1,100 @@ +######################################################################### +# +# Copyright (C) 2024 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 +import os + +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.utils import UploadLimitValidator +from geonode.upload.handlers.common.raster import BaseRasterFileHandler +from geonode.upload.handlers.geotiff.exceptions import InvalidGeoTiffException +from geonode.upload.utils import ImporterRequestAction as ira + +logger = logging.getLogger("importer") + + +class GeoTiffFileHandler(BaseRasterFileHandler): + """ + Handler to import GeoTiff files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_raster_file", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + @property + def supported_file_extension_config(self): + return { + "id": "tiff", + "label": "GeoTIFF", + "format": "raster", + "ext": ["tiff", "tif", "geotiff", "geotif"], + "mimeType": ["image/tiff"], + "optional": ["xml", "sld"], + } + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + base = _data.get("base_file") + if not base: + return False + ext = base.split(".")[-1] if isinstance(base, str) else base.name.split(".")[-1] + return ext in ["tiff", "geotiff", "tif", "geotif"] and BaseRasterFileHandler.can_handle(_data) + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps: + """ + # calling base validation checks + BaseRasterFileHandler.is_valid(files, user) + # getting the upload limit validation + upload_validator = UploadLimitValidator(user) + upload_validator.validate_parallelism_limit_per_user() + + _file = files.get("base_file") + if not _file: + raise InvalidGeoTiffException("base file is not provided") + + filename = os.path.basename(_file) + + if len(filename.split(".")) > 2: + # means that there is a dot other than the one needed for the extension + # if we keep it ogr2ogr raise an error, better to remove it + raise InvalidGeoTiffException("Please remove the additional dots in the filename") + return True diff --git a/geonode/upload/handlers/geotiff/tests.py b/geonode/upload/handlers/geotiff/tests.py new file mode 100644 index 00000000000..882236409a9 --- /dev/null +++ b/geonode/upload/handlers/geotiff/tests.py @@ -0,0 +1,108 @@ +######################################################################### +# +# Copyright (C) 2024 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.test import TestCase +from geonode.upload.handlers.geotiff.exceptions import InvalidGeoTiffException +from django.contrib.auth import get_user_model +from geonode.upload import project_dir +from geonode.upload.models import UploadParallelismLimit +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.base.populate_test_data import create_single_dataset + +from geonode.upload.handlers.geotiff.handler import GeoTiffFileHandler + + +class TestGeoTiffFileHandler(TestCase): + databases = ("default",) + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = GeoTiffFileHandler() + cls.valid_tiff = f"{project_dir}/tests/fixture/test_raster.tif" + cls.valid_files = {"base_file": cls.valid_tiff, "source": "upload"} + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_tiff = {"base_file": "invalid.file.foo"} + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="test_grid", owner=cls.owner) + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 4) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_copy(self): + expected = ( + "start_copy", + "geonode.upload.copy_raster_file", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 4) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): + parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=0) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_tiff, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_pass_with_valid_tif(self): + self.handler.is_valid(files=self.valid_files, user=self.user) + + def test_is_valid_should_raise_exception_if_the_tif_is_invalid(self): + data = {"base_file": "/using/double/dot/in/the/name/is/an/error/file.invalid.tif"} + with self.assertRaises(InvalidGeoTiffException) as _exc: + self.handler.is_valid(files=data, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("Please remove the additional dots in the filename" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_tif_is_invalid_format(self): + with self.assertRaises(InvalidGeoTiffException) as _exc: + self.handler.is_valid(files=self.invalid_tiff, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("Please remove the additional dots in the filename" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_tif_not_provided(self): + with self.assertRaises(InvalidGeoTiffException) as _exc: + self.handler.is_valid(files={"foo": "bar"}, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("base file is not provided" in str(_exc.exception.detail)) + + def test_can_handle_should_return_true_for_tif(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.gpkg"}) + self.assertFalse(actual) diff --git a/geonode/upload/handlers/gpkg/__init__.py b/geonode/upload/handlers/gpkg/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/gpkg/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/gpkg/exceptions.py b/geonode/upload/handlers/gpkg/exceptions.py new file mode 100644 index 00000000000..88224f52a69 --- /dev/null +++ b/geonode/upload/handlers/gpkg/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class InvalidGeopackageException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The geopackage provided is invalid" + default_code = "invalid_gpkg" + category = "importer" diff --git a/geonode/upload/handlers/gpkg/handler.py b/geonode/upload/handlers/gpkg/handler.py new file mode 100644 index 00000000000..1742d12c9e1 --- /dev/null +++ b/geonode/upload/handlers/gpkg/handler.py @@ -0,0 +1,158 @@ +######################################################################### +# +# Copyright (C) 2024 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 geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.upload.utils import UploadLimitValidator +from geopackage_validator.validate import validate +from geonode.upload.handlers.gpkg.exceptions import InvalidGeopackageException +from osgeo import ogr + +from geonode.upload.handlers.common.vector import BaseVectorFileHandler +from geonode.upload.utils import ImporterRequestAction as ira + +logger = logging.getLogger("importer") + + +class GPKGFileHandler(BaseVectorFileHandler): + """ + Handler to import GPK files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + @property + def supported_file_extension_config(self): + return { + "id": "gpkg", + "label": "GeoPackage", + "format": "vector", + "ext": ["gpkg"], + } + + @property + def can_handle_xml_file(self) -> bool: + """ + True or false if the handler is able to handle XML file + By default a common workflow is always defined + To be override if some expection are needed + """ + return False + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + base = _data.get("base_file") + if not base: + return False + return ( + base.endswith(".gpkg") if isinstance(base, str) else base.name.endswith(".gpkg") + ) and BaseVectorFileHandler.can_handle(_data) + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps: + Upload limit: + - raise exception if the layer number of the gpkg is greater than the max upload per user + - raise exception if the actual upload + the gpgk layer is greater than the max upload limit + + Gpkg definition: + Codes table definition is here: https://github.com/PDOK/geopackage-validator#what-does-it-do + RQ1: Layer names must start with a letter, and valid characters are lowercase a-z, numbers or underscores. + RQ2: Layers must have at least one feature. + RQ13: It is required to give all GEOMETRY features the same default spatial reference system + RQ14: The geometry_type_name from the gpkg_geometry_columns table must be one of POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, or MULTIPOLYGON + RQ15: All table geometries must match the geometry_type_name from the gpkg_geometry_columns table + RC18: It is recommended to give all GEOMETRY type columns the same name. + """ + # calling base validation checks + BaseVectorFileHandler.is_valid(files, user) + # getting the upload limit validation + upload_validator = UploadLimitValidator(user) + upload_validator.validate_parallelism_limit_per_user() + actual_upload = upload_validator._get_parallel_uploads_count() + max_upload = upload_validator._get_max_parallel_uploads() + + layers = GPKGFileHandler().get_ogr2ogr_driver().Open(files.get("base_file")) + + if not layers: + raise InvalidGeopackageException("The geopackage provided is invalid") + + layers_count = len(layers) + + if layers_count >= max_upload: + raise UploadParallelismLimitException( + detail=f"The number of layers in the gpkg {layers_count} is greater than " + f"the max parallel upload permitted: {max_upload} " + f"please upload a smaller file" + ) + elif layers_count + actual_upload >= max_upload: + raise UploadParallelismLimitException( + detail=f"With the provided gpkg, the number of max parallel upload will exceed the limit of {max_upload}" + ) + + validator = validate( + gpkg_path=files.get("base_file"), + validations="RQ1, RQ2, RQ13, RQ14, RQ15, RC18", + ) + if not validator[-1]: + error_to_raise = [] + for error in validator[0]: + logger.error(error) + if "locations" in error: + error_to_raise.extend(error["locations"]) + else: + error_to_raise.append(error["validation_description"]) + + raise InvalidGeopackageException(". ".join(error_to_raise)) + + return True + + def get_ogr2ogr_driver(self): + return ogr.GetDriverByName("GPKG") + + def handle_xml_file(self, saved_dataset, _exec): + """ + Not implemented for GPKG, skipping + """ + pass diff --git a/geonode/upload/handlers/gpkg/tasks.py b/geonode/upload/handlers/gpkg/tasks.py new file mode 100644 index 00000000000..4c83d68715a --- /dev/null +++ b/geonode/upload/handlers/gpkg/tasks.py @@ -0,0 +1,39 @@ +######################################################################### +# +# Copyright (C) 2024 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 celery import Task + +from geonode.upload.handlers.utils import evaluate_error + +logger = logging.getLogger("importer") + + +class SingleMessageErrorHandler(Task): + max_retries = 1 + track_started = True + + def on_failure(self, exc, task_id, args, kwargs, einfo): + """ + THis is separated because for gpkg we have a side effect + (we rollback dynamic models and ogr2ogr) + based on this failure step which is not meant for the other + handlers + """ + evaluate_error(self, exc, task_id, args, kwargs, einfo) diff --git a/geonode/upload/handlers/gpkg/tests.py b/geonode/upload/handlers/gpkg/tests.py new file mode 100644 index 00000000000..32770d5c393 --- /dev/null +++ b/geonode/upload/handlers/gpkg/tests.py @@ -0,0 +1,169 @@ +######################################################################### +# +# Copyright (C) 2024 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 shutil +from django.test import TestCase, override_settings +from geonode.upload.handlers.gpkg.exceptions import InvalidGeopackageException +from django.contrib.auth import get_user_model +from geonode.upload.handlers.gpkg.handler import GPKGFileHandler +from geonode.upload import project_dir +from geonode.upload.orchestrator import orchestrator +from geonode.upload.models import UploadParallelismLimit +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.base.populate_test_data import create_single_dataset +from osgeo import ogr +from django_celery_results.models import TaskResult +from geonode.assets.handlers import asset_handler_registry + +from geonode.upload.handlers.gpkg.tasks import SingleMessageErrorHandler + + +class TestGPKGHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = GPKGFileHandler() + cls.valid_gpkg = f"{project_dir}/tests/fixture/valid.gpkg" + cls.invalid_gpkg = f"{project_dir}/tests/fixture/invalid.gpkg" + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_gpkg} + cls.valid_files = {"base_file": cls.valid_gpkg, "source": "upload"} + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="stazioni_metropolitana", owner=cls.owner) + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 4) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_geojson(self): + expected = ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_gpkg_is_invalid(self): + with self.assertRaises(InvalidGeopackageException) as _exc: + self.handler.is_valid(files=self.invalid_files, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("Error layer: INVALID LAYER_name" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): + parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=0) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_files, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_raise_exception_if_layer_are_greater_than_max_parallel_upload( + self, + ): + parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=1) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_files, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_pass_with_valid_gpkg(self): + self.handler.is_valid(files=self.valid_files, user=self.user) + + def test_get_ogr2ogr_driver_should_return_the_expected_driver(self): + expected = ogr.GetDriverByName("GPKG") + actual = self.handler.get_ogr2ogr_driver() + self.assertEqual(type(expected), type(actual)) + + def test_can_handle_should_return_true_for_geopackage(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.file"}) + self.assertFalse(actual) + + @override_settings(MEDIA_ROOT="/tmp/") + def test_single_message_error_handler(self): + # lets copy the file to the temporary folder + # later will be removed + shutil.copy(self.valid_gpkg, "/tmp") + + user = get_user_model().objects.first() + asset_handler = asset_handler_registry.get_default_handler() + + asset = asset_handler.create( + title="Original", + owner=user, + description=None, + type="gpkg", + files=["/tmp/valid.gpkg"], + clone_files=False, + ) + + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={ + "files": {"base_file": "/tmp/valid.gpkg"}, + "skip_existing_layer": True, + "store_spatial_file": False, + "handler_module_path": str(self.handler), + "asset_id": asset.id, + "asset_module_path": f"{asset.__module__}.{asset.__class__.__name__}", + }, + ) + + started_entry = TaskResult.objects.create(task_id=str(exec_id), status="STARTED", task_args=str(exec_id)) + + celery_task_handler = SingleMessageErrorHandler() + """ + The progress evaluation will raise and exception + """ + celery_task_handler.on_failure( + exc=Exception("exception raised"), + task_id=started_entry.task_id, + args=[str(exec_id), "other_args"], + kwargs={}, + einfo=None, + ) + + self.assertEqual("FAILURE", TaskResult.objects.get(task_id=str(exec_id)).status) diff --git a/geonode/upload/handlers/kml/__init__.py b/geonode/upload/handlers/kml/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/kml/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/kml/exceptions.py b/geonode/upload/handlers/kml/exceptions.py new file mode 100644 index 00000000000..1aefb0c15df --- /dev/null +++ b/geonode/upload/handlers/kml/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class InvalidKmlException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The kml provided is invalid" + default_code = "invalid_kml" + category = "importer" diff --git a/geonode/upload/handlers/kml/handler.py b/geonode/upload/handlers/kml/handler.py new file mode 100644 index 00000000000..8ab7f9d00a4 --- /dev/null +++ b/geonode/upload/handlers/kml/handler.py @@ -0,0 +1,153 @@ +######################################################################### +# +# Copyright (C) 2024 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 +import os + +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.upload.utils import UploadLimitValidator +from osgeo import ogr + +from geonode.upload.handlers.common.vector import BaseVectorFileHandler +from geonode.upload.handlers.kml.exceptions import InvalidKmlException +from geonode.upload.utils import ImporterRequestAction as ira + +logger = logging.getLogger("importer") + + +class KMLFileHandler(BaseVectorFileHandler): + """ + Handler to import KML files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + @property + def supported_file_extension_config(self): + return { + "id": "kml", + "label": "KML/KMZ", + "format": "vector", + "ext": ["kml", "kmz"], + } + + @property + def can_handle_xml_file(self) -> bool: + """ + True or false if the handler is able to handle XML file + By default a common workflow is always defined + To be override if some expection are needed + """ + return False + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + BaseVectorFileHandler.can_handle(_data) + base = _data.get("base_file") + if not base: + return False + return ( + base.endswith(".kml") or base.endswith(".kmz") + if isinstance(base, str) + else base.name.endswith(".kml") or base.name.endswith(".kmz") + ) + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps: + Upload limit: + - raise exception if the layer number of the gpkg is greater than the max upload per user + - raise exception if the actual upload + the gpgk layer is greater than the max upload limit + """ + # calling base validation checks + BaseVectorFileHandler.is_valid(files, user) + # getting the upload limit validation + upload_validator = UploadLimitValidator(user) + upload_validator.validate_parallelism_limit_per_user() + actual_upload = upload_validator._get_parallel_uploads_count() + max_upload = upload_validator._get_max_parallel_uploads() + + layers = KMLFileHandler().get_ogr2ogr_driver().Open(files.get("base_file")) + + if not layers: + raise InvalidKmlException("The kml provided is invalid") + + layers_count = len(layers) + + if layers_count >= max_upload: + raise UploadParallelismLimitException( + detail=f"The number of layers in the kml {layers_count} is greater than " + f"the max parallel upload permitted: {max_upload} " + f"please upload a smaller file" + ) + elif layers_count + actual_upload >= max_upload: + raise UploadParallelismLimitException( + detail=f"With the provided kml, the number of max parallel upload will exceed the limit of {max_upload}" + ) + + filename = os.path.basename(files.get("base_file")) + + if len(filename.split(".")) > 2: + # means that there is a dot other than the one needed for the extension + # if we keep it ogr2ogr raise an error, better to remove it + raise InvalidKmlException("Please remove the additional dots in the filename") + return True + + def get_ogr2ogr_driver(self): + return ogr.GetDriverByName("KML") + + def handle_xml_file(self, saved_dataset, _exec): + """ + Not implemented for KML, skipping + """ + pass + + @staticmethod + def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate): + """ + Define the ogr2ogr command to be executed. + This is a default command that is needed to import a vector file + """ + + base_command = BaseVectorFileHandler.create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate) + return f"{base_command } -lco GEOMETRY_NAME={BaseVectorFileHandler().default_geometry_column_name} --config OGR_SKIP LibKML" diff --git a/geonode/upload/handlers/kml/tests.py b/geonode/upload/handlers/kml/tests.py new file mode 100644 index 00000000000..9fe0a873c47 --- /dev/null +++ b/geonode/upload/handlers/kml/tests.py @@ -0,0 +1,115 @@ +######################################################################### +# +# Copyright (C) 2024 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.test import TestCase +from django.contrib.auth import get_user_model +from geonode.upload.handlers.kml.exceptions import InvalidKmlException +from geonode.upload.handlers.kml.handler import KMLFileHandler +from geonode.upload import project_dir +from geonode.upload.models import UploadParallelismLimit +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.base.populate_test_data import create_single_dataset +from osgeo import ogr + + +class TestKMLHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = KMLFileHandler() + cls.valid_kml = f"{project_dir}/tests/fixture/valid.kml" + cls.invalid_kml = f"{project_dir}/tests/fixture/inva.lid.kml" + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_kml} + cls.valid_files = {"base_file": cls.valid_kml, "source": "upload"} + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="extruded_polygon", owner=cls.owner) + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 4) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_geojson(self): + expected = ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_kml_is_invalid(self): + with self.assertRaises(InvalidKmlException) as _exc: + self.handler.is_valid(files=self.invalid_files, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("The kml provided is invalid" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): + parallelism, _ = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=0) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_files, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_raise_exception_if_layer_are_greater_than_max_parallel_upload( + self, + ): + parallelism, _ = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=1) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_files, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_pass_with_valid_gpkg(self): + self.handler.is_valid(files=self.valid_files, user=self.user) + + def test_get_ogr2ogr_driver_should_return_the_expected_driver(self): + expected = ogr.GetDriverByName("KML") + actual = self.handler.get_ogr2ogr_driver() + self.assertEqual(type(expected), type(actual)) + + def test_can_handle_should_return_true_for_kml(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.file"}) + self.assertFalse(actual) diff --git a/geonode/upload/handlers/remote/__init__.py b/geonode/upload/handlers/remote/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/upload/handlers/remote/serializers/__init__.py b/geonode/upload/handlers/remote/serializers/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/remote/serializers/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/remote/serializers/wms.py b/geonode/upload/handlers/remote/serializers/wms.py new file mode 100644 index 00000000000..0b75a380dea --- /dev/null +++ b/geonode/upload/handlers/remote/serializers/wms.py @@ -0,0 +1,35 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework import serializers +from geonode.upload.handlers.common.serializer import RemoteResourceSerializer + + +class RemoteWMSSerializer(RemoteResourceSerializer): + class Meta: + model = RemoteResourceSerializer.Meta.model + ref_name = "RemoteWMSSerializer" + fields = RemoteResourceSerializer.Meta.fields + ( + "lookup", + "bbox", + "parse_remote_metadata", + ) + + lookup = serializers.CharField(required=True) + bbox = serializers.ListField(required=False) + parse_remote_metadata = serializers.BooleanField(required=False, default=False) diff --git a/geonode/upload/handlers/remote/tests/__init__.py b/geonode/upload/handlers/remote/tests/__init__.py new file mode 100755 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/remote/tests/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/remote/tests/test_3dtiles.py b/geonode/upload/handlers/remote/tests/test_3dtiles.py new file mode 100644 index 00000000000..675b65e92f4 --- /dev/null +++ b/geonode/upload/handlers/remote/tests/test_3dtiles.py @@ -0,0 +1,175 @@ +######################################################################### +# +# Copyright (C) 2024 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.test import TestCase +from mock import MagicMock, patch +from geonode.upload.api.exceptions import ImportException +from django.contrib.auth import get_user_model +from geonode.upload.handlers.common.serializer import RemoteResourceSerializer +from geonode.upload.handlers.remote.tiles3d import RemoteTiles3DResourceHandler +from geonode.upload.handlers.tiles3d.exceptions import Invalid3DTilesException +from geonode.upload.orchestrator import orchestrator +from geonode.base.populate_test_data import create_single_dataset +from geonode.resource.models import ExecutionRequest +from geonode.base.models import ResourceBase + + +class TestRemoteTiles3DFileHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = RemoteTiles3DResourceHandler() + cls.valid_url = ( + "https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples/main/1.1/TilesetWithFullMetadata/tileset.json" + ) + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = { + "url": "http://abc123defsadsa.org", + "title": "Remote Title", + "type": "3dtiles", + } + cls.valid_files = { + "url": cls.valid_url, + "title": "Remote Title", + "type": "3dtiles", + } + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="stazioni_metropolitana", owner=cls.owner) + + def test_can_handle_should_return_true_for_remote(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.file"}) + self.assertFalse(actual) + + def test_should_get_the_specific_serializer(self): + actual = self.handler.has_serializer(self.valid_files) + self.assertEqual(type(actual), type(RemoteResourceSerializer)) + + def test_create_error_log(self): + """ + Should return the formatted way for the log of the handler + """ + actual = self.handler.create_error_log( + Exception("my exception"), + "foo_task_name", + *["exec_id", "layer_name", "alternate"], + ) + expected = "Task: foo_task_name raised an error during actions for layer: alternate: my exception" + self.assertEqual(expected, actual) + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 3) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_geojson(self): + expected = ( + "start_copy", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 2) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_url_is_invalid(self): + with self.assertRaises(ImportException) as _exc: + self.handler.is_valid_url(url=self.invalid_files["url"]) + + self.assertIsNotNone(_exc) + self.assertTrue("The provided url is not reachable") + + def test_is_valid_should_pass_with_valid_url(self): + self.handler.is_valid_url(url=self.valid_files["url"]) + + def test_extract_params_from_data(self): + actual, _data = self.handler.extract_params_from_data( + _data={"defaults": '{"url": "http://abc123defsadsa.org", "title": "Remote Title", "type": "3dtiles"}'}, + action="import", + ) + self.assertTrue("title" in actual) + self.assertTrue("url" in actual) + self.assertTrue("type" in actual) + + @patch("geonode.upload.handlers.common.remote.import_orchestrator") + def test_import_resource_should_work(self, patch_upload): + patch_upload.apply_async.side_effect = MagicMock() + try: + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params=self.valid_files, + ) + + # start the resource import + self.handler.import_resource(files=self.valid_files, execution_id=str(exec_id)) + patch_upload.apply_async.assert_called_once() + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_create_geonode_resource_raise_error_if_url_is_not_reachabel(self): + with self.assertRaises(Invalid3DTilesException): + exec_id = orchestrator.create_execution_request( + user=self.owner, + func_name="funct1", + step="step", + input_params={ + "url": "http://abc123defsadsa.org", + "title": "Remote Title", + "type": "3dtiles", + }, + ) + + self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type=ResourceBase, + asset=None, + ) + + def test_create_geonode_resource(self): + exec_id = orchestrator.create_execution_request( + user=self.owner, + func_name="funct1", + step="step", + input_params={ + "url": "https://dummyjson.com/users", + "title": "Remote Title", + "type": "3dtiles", + }, + ) + + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type=ResourceBase, + asset=None, + ) + self.assertIsNotNone(resource) + self.assertEqual(resource.subtype, "3dtiles") diff --git a/geonode/upload/handlers/remote/tests/test_wms.py b/geonode/upload/handlers/remote/tests/test_wms.py new file mode 100644 index 00000000000..5330343b405 --- /dev/null +++ b/geonode/upload/handlers/remote/tests/test_wms.py @@ -0,0 +1,251 @@ +######################################################################### +# +# Copyright (C) 2024 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 collections import namedtuple +from django.test import TestCase +from mock import MagicMock, patch +from geonode.upload.api.exceptions import ImportException +from django.contrib.auth import get_user_model +from geonode.upload.handlers.remote.serializers.wms import RemoteWMSSerializer +from geonode.upload.handlers.remote.wms import RemoteWMSResourceHandler +from geonode.upload.orchestrator import orchestrator +from geonode.resource.models import ExecutionRequest +from geonode.base.models import ResourceBase + + +class TestRemoteWMSResourceHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = RemoteWMSResourceHandler() + cls.valid_url = ( + "https://development.demo.geonode.org/geoserver/ows?service=WMS&version=1.3.0&request=GetCapabilities" + ) + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_payload = { + "url": "http://invalid.com", + "type": "invalid", + "title": "This will fail", + "lookup": "abc124", + "bbox": ["1", "2", "3", "4"], + "parse_remote_metadata": False, + } + cls.valid_payload_with_parse_false = { + "url": cls.valid_url, + "type": "wms", + "title": "This will fail", + "lookup": "abc124", + "bbox": ["1", "2", "3", "4"], + "parse_remote_metadata": False, + } + + cls.valid_payload_with_parse_true = { + "url": cls.valid_url, + "type": "wms", + "title": "This will fail", + "lookup": "abc124", + "bbox": ["1", "2", "3", "4"], + "parse_remote_metadata": True, + } + cls.owner = get_user_model().objects.first() + + def test_can_handle_should_return_true_for_remote(self): + actual = self.handler.can_handle(self.valid_payload_with_parse_false) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle(self.invalid_payload) + self.assertFalse(actual) + + def test_should_get_the_specific_serializer(self): + actual = self.handler.has_serializer(self.valid_payload_with_parse_false) + self.assertEqual(type(actual), type(RemoteWMSSerializer)) + + def test_create_error_log(self): + """ + Should return the formatted way for the log of the handler + """ + actual = self.handler.create_error_log( + Exception("my exception"), + "foo_task_name", + *["exec_id", "layer_name", "alternate"], + ) + expected = "Task: foo_task_name raised an error during actions for layer: alternate: my exception" + self.assertEqual(expected, actual) + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 3) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_geojson(self): + expected = ( + "start_copy", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 2) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_url_is_invalid(self): + with self.assertRaises(ImportException) as _exc: + self.handler.is_valid_url(url=self.invalid_payload["url"]) + + self.assertIsNotNone(_exc) + self.assertTrue("The provided url is not reachable") + + def test_is_valid_should_pass_with_valid_url(self): + self.handler.is_valid_url(url=self.valid_payload_with_parse_false["url"]) + + def test_extract_params_from_data(self): + actual, _data = self.handler.extract_params_from_data( + _data={"defaults": f"{self.valid_payload_with_parse_true}"}, + action="import", + ) + self.assertTrue("title" in actual) + self.assertTrue("url" in actual) + self.assertTrue("type" in actual) + self.assertTrue("lookup" in actual) + self.assertTrue("parse_remote_metadata" in actual) + self.assertTrue("bbox" in actual) + + @patch("geonode.upload.handlers.remote.wms.WmsServiceHandler") + def test_prepare_import_should_not_update_the_execid(self, remote_wms): + """ + prepare_import should update the execid + if the parse_remote_metadata is False + """ + fake_url = MagicMock(shema="http", netloc="fake", path="foo", query="bar") + remote_wms.get_cleaned_url_params.return_value = fake_url, None, None, None + + try: + exec_id = None + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params=self.valid_payload_with_parse_false, + ) + + self.handler.prepare_import(files=[], execution_id=str(exec_id)) + # Title and bbox should not be updated + # since the wms is not called + _exec_obj = orchestrator.get_execution_object(str(exec_id)) + expected_title = "This will fail" + expected_bbox = ["1", "2", "3", "4"] + self.assertEqual(expected_bbox, _exec_obj.input_params["bbox"]) + self.assertEqual(expected_title, _exec_obj.input_params["title"]) + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + @patch("geonode.upload.handlers.remote.wms.WmsServiceHandler") + @patch("geonode.upload.handlers.remote.wms.RemoteWMSResourceHandler.get_wms_resource") + def test_prepare_import_should_update_the_execid(self, get_wms_resource, remote_wms): + """ + prepare_import should update the execid + if the parse_remote_metadata is True + """ + # remote_wms = MagicMock(title="updated_title", bbox=[1,2,3,4]) + fake_url = MagicMock(shema="http", netloc="fake", path="foo", query="bar") + remote_wms.get_cleaned_url_params.return_value = fake_url, None, None, None + + obj = namedtuple("WmsObj", field_names=["title", "boundingBoxWGS84"]) + get_wms_resource.return_value = obj(title="Updated title", boundingBoxWGS84=[9, 109, 5, 1563]) + + try: + exec_id = None + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params=self.valid_payload_with_parse_true, + ) + + self.handler.prepare_import(files=[], execution_id=str(exec_id)) + # Title and bbox should be updated + # since the wms is called + _exec_obj = orchestrator.get_execution_object(str(exec_id)) + expected_title = "Updated title" + expected_bbox = [9, 109, 5, 1563] + self.assertEqual(expected_bbox, _exec_obj.input_params["bbox"]) + self.assertEqual(expected_title, _exec_obj.input_params["title"]) + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + @patch("geonode.upload.handlers.common.remote.import_orchestrator") + def test_import_resource_should_work(self, patch_upload): + patch_upload.apply_async.side_effect = MagicMock() + try: + exec_id = None + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params=self.valid_payload_with_parse_false, + ) + + self.handler.import_resource(files=self.valid_payload_with_parse_false, execution_id=str(exec_id)) + patch_upload.apply_async.assert_called_once() + finally: + if exec_id: + ExecutionRequest.objects.filter(exec_id=exec_id).delete() + + def test_generate_resource_payload(self): + exec_id = orchestrator.create_execution_request( + user=self.owner, + func_name="funct1", + step="step", + input_params=self.valid_payload_with_parse_false, + ) + self.handler.prepare_import(files=[], execution_id=str(exec_id)) + exec_obj = orchestrator.get_execution_object(str(exec_id)) + resource = self.handler.generate_resource_payload( + "layername", + "layeralternate", + _exec=exec_obj, + workspace="geonode", + asset=None, + ) + self.assertIsNotNone(resource) + self.assertEqual(resource["subtype"], "remote") + + def test_create_geonode_resource(self): + exec_id = orchestrator.create_execution_request( + user=self.owner, + func_name="funct1", + step="step", + input_params=self.valid_payload_with_parse_false, + ) + self.handler.prepare_import(files=[], execution_id=str(exec_id)) + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type=ResourceBase, + asset=None, + ) + self.assertIsNotNone(resource) + self.assertEqual(resource.subtype, "remote") + self.assertTrue(ResourceBase.objects.filter(alternate="layeralternate").exists()) diff --git a/geonode/upload/handlers/remote/tiles3d.py b/geonode/upload/handlers/remote/tiles3d.py new file mode 100644 index 00000000000..94f04199abc --- /dev/null +++ b/geonode/upload/handlers/remote/tiles3d.py @@ -0,0 +1,109 @@ +######################################################################### +# +# Copyright (C) 2024 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 + +import requests +from geonode.layers.models import Dataset +from geonode.upload.handlers.common.remote import BaseRemoteResourceHandler +from geonode.upload.handlers.common.serializer import RemoteResourceSerializer +from geonode.upload.handlers.tiles3d.handler import Tiles3DFileHandler +from geonode.upload.orchestrator import orchestrator +from geonode.upload.handlers.tiles3d.exceptions import Invalid3DTilesException +from geonode.base.enumerations import SOURCE_TYPE_REMOTE +from geonode.base.models import ResourceBase + +logger = logging.getLogger("importer") + + +class RemoteTiles3DResourceHandler(BaseRemoteResourceHandler, Tiles3DFileHandler): + + @staticmethod + def has_serializer(data) -> bool: + if "url" in data and "3dtiles" in data.get("type", "").lower(): + return RemoteResourceSerializer + return False + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + if "url" in _data and "3dtiles" in _data.get("type"): + return True + return False + + @staticmethod + def is_valid_url(url, **kwargs): + BaseRemoteResourceHandler.is_valid_url(url) + try: + payload = requests.get(url, timeout=10).json() + # required key described in the specification of 3dtiles + # https://docs.ogc.org/cs/22-025r4/22-025r4.html#toc92 + is_valid = all(key in payload.keys() for key in ("asset", "geometricError", "root")) + + if not is_valid: + raise Invalid3DTilesException( + "The provided 3DTiles is not valid, some of the mandatory keys are missing. Mandatory keys are: 'asset', 'geometricError', 'root'" + ) + + Tiles3DFileHandler.validate_3dtile_payload(payload=payload) + + except Exception as e: + raise Invalid3DTilesException(e) + + return True + + def create_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: Dataset = ResourceBase, + asset=None, + ): + resource = super().create_geonode_resource(layer_name, alternate, execution_id, resource_type, asset) + _exec = orchestrator.get_execution_object(exec_id=execution_id) + try: + js_file = requests.get(_exec.input_params.get("url"), timeout=10).json() + except Exception as e: + raise Invalid3DTilesException(e) + + if not js_file: + raise Invalid3DTilesException("The JSON file returned by the URL is empty") + + if self._has_region(js_file): + resource = self.set_bbox_from_region(js_file, resource=resource) + elif self._has_sphere(js_file): + resource = self.set_bbox_from_boundingVolume_sphere(js_file, resource=resource) + else: + resource = self.set_bbox_from_boundingVolume(js_file, resource=resource) + + return resource + + def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspace, **kwargs): + return dict( + resource_type="dataset", + subtype=kwargs.get("type"), + sourcetype=SOURCE_TYPE_REMOTE, + alternate=alternate, + dirty_state=True, + title=kwargs.get("title", layer_name), + owner=_exec.user, + ) diff --git a/geonode/upload/handlers/remote/wms.py b/geonode/upload/handlers/remote/wms.py new file mode 100644 index 00000000000..1ac7011b4ef --- /dev/null +++ b/geonode/upload/handlers/remote/wms.py @@ -0,0 +1,163 @@ +######################################################################### +# +# Copyright (C) 2024 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.conf import settings +from geonode.layers.models import Dataset +from geonode.upload.handlers.common.remote import BaseRemoteResourceHandler +from geonode.services import enumerations +from geonode.base.enumerations import SOURCE_TYPE_REMOTE +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.handlers.remote.serializers.wms import RemoteWMSSerializer +from geonode.upload.orchestrator import orchestrator +from geonode.harvesting.harvesters.wms import WebMapService +from geonode.services.serviceprocessors.wms import WmsServiceHandler +from geonode.resource.manager import resource_manager + +logger = logging.getLogger("importer") + + +class RemoteWMSResourceHandler(BaseRemoteResourceHandler): + + @staticmethod + def has_serializer(data) -> bool: + """ + Return the custom serializer for the WMS + """ + if "url" in data and enumerations.WMS in data.get("type", "").upper(): + return RemoteWMSSerializer + return False + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + if "url" in _data and enumerations.WMS.lower() in _data.get("type", "").lower(): + return True + return False + + @staticmethod + def extract_params_from_data(_data, action=None): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + payload, original_data = BaseRemoteResourceHandler.extract_params_from_data(_data, action=action) + if action != exa.COPY.value: + payload["lookup"] = original_data.pop("lookup", None) + payload["bbox"] = original_data.pop("bbox", None) + payload["parse_remote_metadata"] = original_data.pop("parse_remote_metadata", None) + + return payload, original_data + + def prepare_import(self, files, execution_id, **kwargs): + """ + If the title and bbox must be retrieved from the remote resource + we take them before starting the import, so we can keep the default + workflow behaviour + """ + _exec = orchestrator.get_execution_object(exec_id=execution_id) + cleaned_url, _, _, _ = WmsServiceHandler.get_cleaned_url_params(_exec.input_params.get("url")) + parsed_url = f"{cleaned_url.scheme}://{cleaned_url.netloc}{cleaned_url.path}" + ows_url = f"{parsed_url}?{cleaned_url.query}" + to_update = { + "ows_url": ows_url, + "parsed_url": parsed_url, + "remote_resource_id": _exec.input_params.get("lookup", None), + } + if _exec.input_params.get("parse_remote_metadata", False): + try: + wms_resource = self.get_wms_resource(_exec) + to_update.update( + { + "title": wms_resource.title, + "bbox": wms_resource.boundingBoxWGS84, + } + ) + except Exception as e: + logger.error(f"Error during the fetch of the WSM details, please check the log {e}") + raise e + + _exec.input_params.update(to_update) + _exec.save() + + def get_wms_resource(self, _exec): + _, wms = WebMapService(_exec.input_params.get("url")) + wms_resource = wms[_exec.input_params.get("lookup")] + return wms_resource + + def generate_alternate( + self, + layer_name, + execution_id, + should_be_overwritten, + payload_alternate, + user_datasets, + dataset_exists, + ): + """ + For WMS we dont want to generate an alternate, otherwise we cannot use + the alternate to lookup the layer in the remote service + """ + return layer_name, payload_alternate + + def create_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: Dataset = ..., + asset=None, + ): + """ + Use the default RemoteResourceHandler to create the geonode resource + after that, we assign the bbox and re-generate the thumbnail + """ + resource = super().create_geonode_resource(layer_name, alternate, execution_id, Dataset, asset) + _exec = orchestrator.get_execution_object(execution_id) + remote_bbox = _exec.input_params.get("bbox") + if remote_bbox: + resource.set_bbox_polygon(remote_bbox, "EPSG:4326") + resource_manager.set_thumbnail(None, instance=resource) + return resource + + def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspace, **kwargs): + """ + Here are returned all the information required to generate the geonode resource + inclusing the OWS url + """ + return dict( + resource_type="dataset", + subtype="remote", + sourcetype=SOURCE_TYPE_REMOTE, + alternate=alternate, + dirty_state=True, + title=kwargs.get("title", layer_name), + name=alternate, + owner=_exec.user, + workspace=getattr(settings, "DEFAULT_WORKSPACE", "geonode"), + store=_exec.input_params.get("parsed_url") + .encode("utf-8", "ignore") + .decode("utf-8") + .replace(".", "") + .replace("/", ""), + ptype="gxp_wmscsource", + ows_url=_exec.input_params.get("ows_url"), + ) diff --git a/geonode/upload/handlers/shapefile/__init__.py b/geonode/upload/handlers/shapefile/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/shapefile/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/shapefile/exceptions.py b/geonode/upload/handlers/shapefile/exceptions.py new file mode 100644 index 00000000000..9b8bed57669 --- /dev/null +++ b/geonode/upload/handlers/shapefile/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class InvalidShapeFileException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The shapefile provided is invalid" + default_code = "invalid_shapefile" + category = "importer" diff --git a/geonode/upload/handlers/shapefile/handler.py b/geonode/upload/handlers/shapefile/handler.py new file mode 100644 index 00000000000..d93214a03bc --- /dev/null +++ b/geonode/upload/handlers/shapefile/handler.py @@ -0,0 +1,212 @@ +######################################################################### +# +# Copyright (C) 2024 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 ast +import json +import logging +import codecs +from geonode.utils import get_supported_datasets_file_types +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.utils import UploadLimitValidator +from geonode.upload.handlers.common.vector import BaseVectorFileHandler +from osgeo import ogr +from pathlib import Path + +from geonode.upload.handlers.shapefile.exceptions import InvalidShapeFileException +from geonode.upload.handlers.shapefile.serializer import OverwriteShapeFileSerializer, ShapeFileSerializer +from geonode.upload.utils import ImporterRequestAction as ira + +logger = logging.getLogger("importer") + + +class ShapeFileHandler(BaseVectorFileHandler): + """ + Handler to import Shapefile files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + @property + def supported_file_extension_config(self): + return { + "id": "shp", + "label": "ESRI Shapefile", + "format": "vector", + "ext": ["shp"], + "requires": ["shp", "prj", "dbf", "shx"], + "optional": ["xml", "sld", "cpg", "cst"], + } + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + base = _data.get("base_file") + if not base: + return False + ext = base.split(".")[-1] if isinstance(base, str) else base.name.split(".")[-1] + return ext in ["shp"] and BaseVectorFileHandler.can_handle(_data) + + @staticmethod + def has_serializer(data) -> bool: + _base = data.get("base_file") + if not _base: + return False + if _base.endswith("shp") if isinstance(_base, str) else _base.name.endswith("shp"): + is_overwrite_flow = data.get("overwrite_existing_layer", False) + if isinstance(is_overwrite_flow, str): + is_overwrite_flow = ast.literal_eval(is_overwrite_flow.title()) + return OverwriteShapeFileSerializer if is_overwrite_flow else ShapeFileSerializer + return False + + @staticmethod + def extract_params_from_data(_data, action=None): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + if action == exa.COPY.value: + title = json.loads(_data.get("defaults")) + return {"title": title.pop("title")}, _data + + additional_params = { + "skip_existing_layers": _data.pop("skip_existing_layers", "False"), + "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), + "resource_pk": _data.pop("resource_pk", None), + "store_spatial_file": _data.pop("store_spatial_files", "True"), + "source": _data.pop("source", "upload"), + } + + return additional_params, _data + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps: + """ + # getting the upload limit validation + upload_validator = UploadLimitValidator(user) + upload_validator.validate_parallelism_limit_per_user() + + _file = files.get("base_file") + if not _file: + raise InvalidShapeFileException("base file is not provided") + + _filename = Path(_file).stem + + _shp_ext_needed = [x["requires"] for x in get_supported_datasets_file_types() if x["id"] == "shp"][0] + + """ + Check if the ext required for the shape file are available in the files uploaded + by the user + """ + is_valid = all( + map( + lambda x: any( + ( + _ext.endswith(f"{_filename}.{x}") + if isinstance(_ext, str) + else _ext.name.endswith(f"{_filename}.{x}") + ) + for _ext in files.values() + ), + _shp_ext_needed, + ) + ) + if not is_valid: + raise InvalidShapeFileException( + detail=f"Some file is missing files with the same name and with the following extension are required: {_shp_ext_needed}" + ) + + return True + + def get_ogr2ogr_driver(self): + return ogr.GetDriverByName("ESRI Shapefile") + + @staticmethod + def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate): + """ + Define the ogr2ogr command to be executed. + This is a default command that is needed to import a vector file + """ + base_command = BaseVectorFileHandler.create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate) + layers = ogr.Open(files.get("base_file")) + layer = layers.GetLayer(original_name) + + encoding = ShapeFileHandler._get_encoding(files) + + additional_options = [] + if layer is not None and "Point" not in ogr.GeometryTypeToName(layer.GetGeomType()): + additional_options.append("-nlt PROMOTE_TO_MULTI") + if encoding: + additional_options.append(f"-lco ENCODING={encoding}") + + return ( + f"{base_command } -lco precision=no -lco GEOMETRY_NAME={BaseVectorFileHandler().default_geometry_column_name} " + + " ".join(additional_options) + ) + + @staticmethod + def _get_encoding(files): + if files.get("cpg_file"): + # prefer cpg file which is handled by gdal + return None + + encoding = None + if files.get("cst_file"): + # GeoServer exports cst-file + encoding_file = files.get("cst_file") + with open(encoding_file, "r") as f: + encoding = f.read() + try: + codecs.lookup(encoding) + except LookupError as e: + encoding = None + logger.error(f"Will ignore invalid encoding: {e}") + return encoding + + def promote_to_multi(self, geometry_name): + """ + If needed change the name of the geometry, by promoting it to Multi + example if is Point -> MultiPoint + Needed for the shapefiles + """ + if "Multi" not in geometry_name and "Point" not in geometry_name: + return f"Multi {geometry_name.title()}" + return geometry_name diff --git a/geonode/upload/handlers/shapefile/serializer.py b/geonode/upload/handlers/shapefile/serializer.py new file mode 100644 index 00000000000..cf7aa407436 --- /dev/null +++ b/geonode/upload/handlers/shapefile/serializer.py @@ -0,0 +1,65 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework import serializers +from dynamic_rest.serializers import DynamicModelSerializer +from geonode.base.models import ResourceBase + + +class ShapeFileSerializer(DynamicModelSerializer): + class Meta: + ref_name = "ShapeFileSerializer" + model = ResourceBase + view_name = "importer_upload" + fields = ( + "base_file", + "dbf_file", + "shx_file", + "prj_file", + "xml_file", + "sld_file", + "store_spatial_files", + "overwrite_existing_layer", + "skip_existing_layers", + "source", + ) + + base_file = serializers.FileField() + dbf_file = serializers.FileField() + shx_file = serializers.FileField() + prj_file = serializers.FileField() + xml_file = serializers.FileField(required=False) + sld_file = serializers.FileField(required=False) + store_spatial_files = serializers.BooleanField(required=False, default=True) + overwrite_existing_layer = serializers.BooleanField(required=False, default=False) + skip_existing_layers = serializers.BooleanField(required=False, default=False) + source = serializers.CharField(required=False, default="upload") + + +class OverwriteShapeFileSerializer(ShapeFileSerializer): + class Meta: + ref_name = "ShapeFileSerializer" + model = ResourceBase + view_name = "importer_upload" + fields = ShapeFileSerializer.Meta.fields + ( + "overwrite_existing_layer", + "resource_pk", + ) + + overwrite_existing_layer = serializers.BooleanField(required=True) + resource_pk = serializers.IntegerField(required=True) diff --git a/geonode/upload/handlers/shapefile/tests.py b/geonode/upload/handlers/shapefile/tests.py new file mode 100644 index 00000000000..f643306bf7e --- /dev/null +++ b/geonode/upload/handlers/shapefile/tests.py @@ -0,0 +1,178 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +import uuid + +from django.conf import settings +import gisdata +from django.contrib.auth import get_user_model +from django.test import TestCase +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.upload.models import UploadParallelismLimit +from mock import MagicMock, patch, mock_open +from geonode.upload import project_dir +from geonode.upload.handlers.common.vector import import_with_ogr2ogr +from geonode.upload.handlers.shapefile.handler import ShapeFileHandler +from osgeo import ogr + +from geonode.upload.handlers.shapefile.serializer import ShapeFileSerializer + + +class TestShapeFileFileHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = ShapeFileHandler() + file_path = gisdata.VECTOR_DATA + filename = os.path.join(file_path, "san_andres_y_providencia_highway.shp") + cls.valid_shp = { + "base_file": filename, + "dbf_file": f"{file_path}/san_andres_y_providencia_highway.dbf", + "prj_file": f"{file_path}/san_andres_y_providencia_highway.prj", + "shx_file": f"{file_path}/san_andres_y_providencia_highway.shx", + "source": "upload", + } + cls.invalid_shp = f"{project_dir}/tests/fixture/invalid.geojson" + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_shp} + cls.owner = get_user_model().objects.first() + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 4) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_copy_task_list_is_the_expected_one(self): + expected = ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): + parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=0) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_shp, user=self.user) + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_promote_to_multi(self): + # point should be keep as point + actual = self.handler.promote_to_multi("Point") + self.assertEqual("Point", actual) + # polygon should be changed into multipolygon + actual = self.handler.promote_to_multi("Polygon") + self.assertEqual("Multi Polygon", actual) + + # linestring should be changed into multilinestring + actual = self.handler.promote_to_multi("Linestring") + self.assertEqual("Multi Linestring", actual) + + # if is already multi should be kept + actual = self.handler.promote_to_multi("Multi Point") + self.assertEqual("Multi Point", actual) + + def test_is_valid_should_pass_with_valid_shp(self): + self.handler.is_valid(files=self.valid_shp, user=self.user) + + def test_get_ogr2ogr_driver_should_return_the_expected_driver(self): + expected = ogr.GetDriverByName("ESRI Shapefile") + actual = self.handler.get_ogr2ogr_driver() + self.assertEqual(type(expected), type(actual)) + + def test_can_handle_should_return_true_for_shp(self): + actual = self.handler.can_handle(self.valid_shp) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.prj"}) + self.assertFalse(actual) + + def test_should_get_the_specific_serializer(self): + actual = self.handler.has_serializer(self.valid_shp) + self.assertEqual(type(actual), type(ShapeFileSerializer)) + + def test_should_NOT_get_the_specific_serializer(self): + actual = self.handler.has_serializer(self.invalid_files) + self.assertFalse(actual) + + def test_should_create_ogr2ogr_command_with_encoding_from_cst(self): + shp_with_cst = self.valid_shp.copy() + cst_file = self.valid_shp["base_file"].replace("shp", "cst") + shp_with_cst["cst_file"] = cst_file + patch_location = "geonode.upload.handlers.shapefile.handler.open" + with patch(patch_location, new=mock_open(read_data="UTF-8")) as _file: + actual = self.handler.create_ogr2ogr_command(shp_with_cst, "a", False, "a") + + _file.assert_called_once_with(cst_file, "r") + self.assertIn("ENCODING=UTF-8", actual) + + @patch("geonode.upload.handlers.common.vector.Popen") + def test_import_with_ogr2ogr_without_errors_should_call_the_right_command(self, _open): + _uuid = uuid.uuid4() + + comm = MagicMock() + comm.communicate.return_value = b"", b"" + _open.return_value = comm + + _task, alternate, execution_id = import_with_ogr2ogr( + execution_id=str(_uuid), + files=self.valid_shp, + original_name="dataset", + handler_module_path=str(self.handler), + ovverwrite_layer=False, + alternate="alternate", + ) + + self.assertEqual("ogr2ogr", _task) + self.assertEqual(alternate, "alternate") + self.assertEqual(str(_uuid), execution_id) + + _datastore = settings.DATABASES["datastore"] + _open.assert_called_once() + _open.assert_called_with( + "/usr/bin/ogr2ogr --config PG_USE_COPY YES -f PostgreSQL PG:\" dbname='test_geonode_data' host=" + + os.getenv("DATABASE_HOST", "localhost") + + " port=5432 user='" + + _datastore["USER"] + + "' password='" + + _datastore["PASSWORD"] + + '\' " "' + + self.valid_shp.get("base_file") + + '" -nln alternate "dataset" -lco precision=no -lco GEOMETRY_NAME=geometry ', + stdout=-1, + stderr=-1, + shell=True, # noqa + ) diff --git a/geonode/upload/handlers/sld/__init__.py b/geonode/upload/handlers/sld/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/sld/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/sld/exceptions.py b/geonode/upload/handlers/sld/exceptions.py new file mode 100644 index 00000000000..ca6ae73bca0 --- /dev/null +++ b/geonode/upload/handlers/sld/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class InvalidSldException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The sld provided is invalid" + default_code = "invalid_sld" + category = "importer" diff --git a/geonode/upload/handlers/sld/handler.py b/geonode/upload/handlers/sld/handler.py new file mode 100644 index 00000000000..c6cd25863a4 --- /dev/null +++ b/geonode/upload/handlers/sld/handler.py @@ -0,0 +1,94 @@ +######################################################################### +# +# Copyright (C) 2024 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 geonode.resource.manager import resource_manager +from geonode.upload.handlers.common.metadata import MetadataFileHandler +from geonode.upload.handlers.sld.exceptions import InvalidSldException +from owslib.etree import etree as dlxml + +logger = logging.getLogger("importer") + + +class SLDFileHandler(MetadataFileHandler): + """ + Handler to import SLD files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + @property + def supported_file_extension_config(self): + return { + "id": "sld", + "label": "Styled Layer Descriptor (SLD)", + "format": "metadata", + "ext": ["sld"], + "mimeType": ["application/json"], + "needsFiles": [ + "shp", + "prj", + "dbf", + "shx", + "csv", + "tiff", + "zip", + "xml", + "geojson", + ], + } + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + base = _data.get("base_file") + if not base: + return False + return ( + base.endswith(".sld") if isinstance(base, str) else base.name.endswith(".sld") + ) and MetadataFileHandler.can_handle(_data) + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps + """ + # calling base validation checks + try: + with open(files.get("base_file")) as _xml: + dlxml.fromstring(_xml.read().encode()) + except Exception as err: + raise InvalidSldException(f"Uploaded document is not SLD or is invalid: {str(err)}") + return True + + def handle_metadata_resource(self, _exec, dataset, original_handler): + if original_handler.can_handle_sld_file: + original_handler.handle_sld_file(dataset, _exec) + else: + _path = _exec.input_params.get("files", {}).get("sld_file", _exec.input_params.get("base_file", {})) + resource_manager.exec( + "set_style", + None, + instance=dataset, + sld_file=_exec.input_params.get("files", {}).get("sld_file", ""), + sld_uploaded=True if _path else False, + vals={"dirty_state": True}, + ) diff --git a/geonode/upload/handlers/sld/tests.py b/geonode/upload/handlers/sld/tests.py new file mode 100644 index 00000000000..1864ba2dee1 --- /dev/null +++ b/geonode/upload/handlers/sld/tests.py @@ -0,0 +1,107 @@ +######################################################################### +# +# Copyright (C) 2024 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 shutil +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import TestCase, override_settings +from geonode.base.populate_test_data import create_single_dataset +from geonode.upload import project_dir +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload.orchestrator import orchestrator +from geonode.upload.handlers.sld.exceptions import InvalidSldException +from geonode.upload.handlers.sld.handler import SLDFileHandler + + +class TestSLDFileHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = SLDFileHandler() + cls.valid_sld = f"{settings.PROJECT_ROOT}/base/fixtures/test_sld.sld" + cls.invalid_sld = f"{project_dir}/tests/fixture/invalid.gpkg" + + shutil.copy(cls.valid_sld, "/tmp") + + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_sld, "sld_file": cls.invalid_sld} + cls.valid_files = { + "base_file": "/tmp/test_sld.sld", + "sld_file": "/tmp/test_sld.sld", + "source": "resource_file_upload", + } + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="sld_dataset", owner=cls.owner) + + def setUp(self) -> None: + shutil.copy(self.valid_sld, "/tmp") + super().setUp() + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 2) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_is_valid_should_raise_exception_if_the_sld_is_invalid(self): + with self.assertRaises(InvalidSldException) as _exc: + self.handler.is_valid(files=self.invalid_files, user="user") + + self.assertIsNotNone(_exc) + self.assertTrue("Uploaded document is not SLD or is invalid" in str(_exc.exception.detail)) + + def test_is_valid_should_pass_with_valid_sld(self): + self.handler.is_valid(files=self.valid_files, user="user") + + def test_can_handle_should_return_true_for_sld(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.file"}) + self.assertFalse(actual) + + @override_settings(MEDIA_ROOT="/tmp/") + def test_can_successfully_import_metadata_file(self): + + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={ + "files": self.valid_files, + "resource_pk": self.layer.pk, + "skip_existing_layer": True, + "handler_module_path": str(self.handler), + }, + ) + ResourceHandlerInfo.objects.create( + resource=self.layer, + handler_module_path="geonode.upload.handlers.shapefile.handler.ShapeFileHandler", + ) + + self.assertEqual(self.layer.title, "sld_dataset") + + self.handler.import_resource({}, str(exec_id)) + + self.layer.refresh_from_db() + self.assertEqual(self.layer.title, "sld_dataset") diff --git a/geonode/upload/handlers/tests.py b/geonode/upload/handlers/tests.py new file mode 100644 index 00000000000..20362b7b26d --- /dev/null +++ b/geonode/upload/handlers/tests.py @@ -0,0 +1,83 @@ +######################################################################### +# +# Copyright (C) 2024 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.test import TestCase +from geonode.base.populate_test_data import create_single_dataset +from django.contrib.auth import get_user_model +from dynamic_models.models import ModelSchema +from geonode.upload.handlers.utils import ( + create_alternate, + drop_dynamic_model_schema, + should_be_imported, +) + + +class TestHandlersUtils(TestCase): + databases = ("default", "datastore") + + def test_should_be_imported_return_true(self): + """ + Should return true because a dataset with the same name for the user + already exsits, but was not asked to skip the existing + """ + user, _ = get_user_model().objects.get_or_create(username="admin") + dataset = create_single_dataset(name="single_dataset", owner=user) + result = should_be_imported(layer=dataset.name, user=user) + self.assertTrue(result) + + def test_should_be_imported_return_true_with_no_dataset(self): + """ + Return True because the dataset does not exists + """ + user, _ = get_user_model().objects.get_or_create(username="admin") + result = should_be_imported(layer="dataset_name", user=user) + self.assertTrue(result) + + def test_should_be_imported_return_false(self): + """ + Should return false because a dataset with the same name for the user + already exsits and is required to skipt the existing + """ + user, _ = get_user_model().objects.get_or_create(username="admin") + dataset = create_single_dataset(name="single_dataset", owner=user) + result = should_be_imported(layer=dataset.name, user=user, skip_existing_layer=True) + self.assertFalse(result) + + def test_create_alternate_shuould_appen_an_hash(self): + actual = create_alternate(layer_name="name", execution_id="1234") + self.assertTrue(actual.startswith("name_")) + self.assertTrue(len(actual) <= 63) + + def test_create_alternate_shuould_cut_the_hash_if_is_longer_than_63(self): + actual = create_alternate( + layer_name="this_is_a_really_long_name_for_a_layer_but_we_need_it_for_test_the_function", + execution_id="1234", + ) + self.assertEqual("this_is_a_really_long_name_for_a_layer_but_we_need", actual[:50]) + self.assertTrue(len(actual) <= 63) + + def test_drop_dynamic_model_schema(self): + _model_schema = ModelSchema(name="model_schema", db_name="datastore", managed=True) + _model_schema.save() + + self.assertTrue(ModelSchema.objects.filter(name="model_schema").exists()) + + # dropping the schema + drop_dynamic_model_schema(schema_model=_model_schema) + + self.assertFalse(ModelSchema.objects.filter(name="model_schema").exists()) diff --git a/geonode/upload/handlers/tiles3d/__init__.py b/geonode/upload/handlers/tiles3d/__init__.py new file mode 100755 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/tiles3d/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/tiles3d/exceptions.py b/geonode/upload/handlers/tiles3d/exceptions.py new file mode 100755 index 00000000000..0e27a6aa4ae --- /dev/null +++ b/geonode/upload/handlers/tiles3d/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class Invalid3DTilesException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The 3dtiles provided is invalid" + default_code = "invalid_3dtiles" + category = "importer" diff --git a/geonode/upload/handlers/tiles3d/handler.py b/geonode/upload/handlers/tiles3d/handler.py new file mode 100755 index 00000000000..9cb2db7e426 --- /dev/null +++ b/geonode/upload/handlers/tiles3d/handler.py @@ -0,0 +1,319 @@ +######################################################################### +# +# Copyright (C) 2024 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 +import os +from pathlib import Path +import math +from geonode.layers.models import Dataset +from geonode.resource.enumerator import ExecutionRequestAction as exa +from geonode.upload.utils import UploadLimitValidator +from geonode.upload.handlers.tiles3d.utils import box_to_wgs84, sphere_to_wgs84 +from geonode.upload.orchestrator import orchestrator +from geonode.upload.celery_tasks import import_orchestrator +from geonode.upload.handlers.common.vector import BaseVectorFileHandler +from geonode.upload.handlers.utils import create_alternate, should_be_imported +from geonode.upload.utils import ImporterRequestAction as ira +from geonode.base.models import ResourceBase +from geonode.upload.handlers.tiles3d.exceptions import Invalid3DTilesException + +logger = logging.getLogger("importer") + + +class Tiles3DFileHandler(BaseVectorFileHandler): + """ + Handler to import 3Dtiles files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + ACTIONS = { + exa.IMPORT.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + + @property + def supported_file_extension_config(self): + return { + "id": "3dtiles", + "label": "3D Tiles", + "format": "vector", + "ext": ["json"], + "optional": ["xml", "sld"], + } + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + base = _data.get("base_file") + if not base: + return False + ext = base.split(".")[-1] if isinstance(base, str) else base.name.split(".")[-1] + input_filename = os.path.basename(base if isinstance(base, str) else base.name) + if ext in ["json"] and "tileset.json" in input_filename: + return True + return False + + @staticmethod + def is_valid(files, user, **kwargs): + """ + Define basic validation steps: + """ + # calling base validation checks + BaseVectorFileHandler.is_valid(files, user) + # getting the upload limit validation + upload_validator = UploadLimitValidator(user) + upload_validator.validate_parallelism_limit_per_user() + + _file = files.get("base_file") + if not _file: + raise Invalid3DTilesException("base file is not provided") + + filename = os.path.basename(_file) + + if len(filename.split(".")) > 2: + # means that there is a dot other than the one needed for the extension + # if we keep it ogr2ogr raise an error, better to remove it + raise Invalid3DTilesException("Please remove the additional dots in the filename") + + try: + with open(_file, "r") as _readed_file: + _file = json.loads(_readed_file.read()) + # required key described in the specification of 3dtiles + # https://docs.ogc.org/cs/22-025r4/22-025r4.html#toc92 + is_valid = all(key in _file.keys() for key in ("asset", "geometricError", "root")) + + if not is_valid: + raise Invalid3DTilesException( + "The provided 3DTiles is not valid, some of the mandatory keys are missing. Mandatory keys are: 'asset', 'geometricError', 'root'" + ) + + Tiles3DFileHandler.validate_3dtile_payload(payload=_file) + + except Exception as e: + raise Invalid3DTilesException(e) + + return True + + @staticmethod + def validate_3dtile_payload(payload): + # if the keys are there, let's check if the mandatory child are there too + asset = payload.get("asset", {}).get("version", None) + if not asset: + raise Invalid3DTilesException("The mandatory 'version' for the key 'asset' is missing") + volume = payload.get("root", {}).get("boundingVolume", None) + if not volume: + raise Invalid3DTilesException("The mandatory 'boundingVolume' for the key 'root' is missing") + + error = payload.get("root", {}).get("geometricError", None) + if error is None: + raise Invalid3DTilesException("The mandatory 'geometricError' for the key 'root' is missing") + + @staticmethod + def extract_params_from_data(_data, action=None): + """ + Remove from the _data the params that needs to save into the executionRequest object + all the other are returned + """ + if action == exa.COPY.value: + title = json.loads(_data.get("defaults")) + return {"title": title.pop("title")}, _data + + return { + "skip_existing_layers": _data.pop("skip_existing_layers", "False"), + "store_spatial_file": _data.pop("store_spatial_files", "True"), + "source": _data.pop("source", "upload"), + "original_zip_name": _data.pop("original_zip_name", None), + "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), + }, _data + + def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: + logger.info("Total number of layers available: 1") + + _exec = self._get_execution_request_object(execution_id) + + _input = {**_exec.input_params, **{"total_layers": 1}} + + orchestrator.update_execution_request_status(execution_id=str(execution_id), input_params=_input) + filename = _exec.input_params.get("original_zip_name") or Path(files.get("base_file")).stem + # start looping on the layers available + layer_name = self.fixup_name(filename) + should_be_overwritten = _exec.input_params.get("overwrite_existing_layer") + # should_be_imported check if the user+layername already exists or not + if should_be_imported( + layer_name, + _exec.user, + skip_existing_layer=_exec.input_params.get("skip_existing_layer"), + overwrite_existing_layer=should_be_overwritten, + ): + + user_datasets = ResourceBase.objects.filter(owner=_exec.user, alternate=layer_name) + + dataset_exists = user_datasets.exists() + + if dataset_exists and should_be_overwritten: + layer_name, alternate = ( + layer_name, + user_datasets.first().alternate.split(":")[-1], + ) + elif not dataset_exists: + alternate = layer_name + else: + alternate = create_alternate(layer_name, execution_id) + + import_orchestrator.apply_async( + ( + files, + execution_id, + str(self), + "geonode.upload.import_resource", + layer_name, + alternate, + exa.IMPORT.value, + ) + ) + return layer_name, alternate, execution_id + + def create_geonode_resource( + self, + layer_name: str, + alternate: str, + execution_id: str, + resource_type: Dataset = ..., + asset=None, + ): + # we want just the tileset.json as location of the asset + asset.location = [path for path in asset.location if "tileset.json" in path] + asset.save() + + resource = super().create_geonode_resource(layer_name, alternate, execution_id, ResourceBase, asset) + + # fixing-up bbox for the 3dtile object + js_file = None + with open(asset.location[0]) as _file: + js_file = json.loads(_file.read()) + + if not js_file: + return resource + + if self._has_region(js_file): + resource = self.set_bbox_from_region(js_file, resource=resource) + elif self._has_sphere(js_file): + resource = self.set_bbox_from_boundingVolume_sphere(js_file, resource=resource) + else: + resource = self.set_bbox_from_boundingVolume(js_file, resource=resource) + + return resource + + def generate_resource_payload(self, layer_name, alternate, asset, _exec, workspace): + return dict( + resource_type="dataset", + subtype="3dtiles", + dirty_state=True, + title=layer_name, + owner=_exec.user, + asset=asset, + link_type="uploaded", + extension="3dtiles", + alternate=alternate, + ) + + def set_bbox_from_region(self, js_file, resource): + # checking if the region is inside the json file + region = js_file.get("root", {}).get("boundingVolume", {}).get("region", None) + if not region: + logger.info(f"No region found, the BBOX will not be updated for 3dtiles: {resource.title}") + return resource + west, south, east, nord = region[:4] + # [xmin, ymin, xmax, ymax] + resource.set_bbox_polygon( + bbox=[ + math.degrees(west), + math.degrees(south), + math.degrees(east), + math.degrees(nord), + ], + srid="EPSG:4326", + ) + + return resource + + def set_bbox_from_boundingVolume(self, js_file, resource): + transform_raw = js_file.get("root", {}).get("transform", []) + box_raw = js_file.get("root", {}).get("boundingVolume", {}).get("box", None) + + if not box_raw or (not transform_raw and not box_raw): + # skipping if values are missing from the json file + return resource + + result = box_to_wgs84(box_raw, transform_raw) + # [xmin, ymin, xmax, ymax] + resource.set_bbox_polygon( + bbox=[ + result["minx"], + result["miny"], + result["maxx"], + result["maxy"], + ], + srid="EPSG:4326", + ) + + return resource + + def set_bbox_from_boundingVolume_sphere(self, js_file, resource): + transform_raw = js_file.get("root", {}).get("transform", []) + sphere_raw = js_file.get("root", {}).get("boundingVolume", {}).get("sphere", None) + + if not sphere_raw or (not transform_raw and not sphere_raw): + # skipping if values are missing from the json file + return resource + if not transform_raw and (sphere_raw[0], sphere_raw[1], sphere_raw[2]) == (0, 0, 0): + return resource + result = sphere_to_wgs84(sphere_raw, transform_raw) + # [xmin, ymin, xmax, ymax] + resource.set_bbox_polygon( + bbox=[ + result["minx"], + result["miny"], + result["maxx"], + result["maxy"], + ], + srid="EPSG:4326", + ) + + return resource + + def _has_region(self, js_file): + return js_file.get("root", {}).get("boundingVolume", {}).get("region", None) + + def _has_sphere(self, js_file): + return js_file.get("root", {}).get("boundingVolume", {}).get("sphere", None) diff --git a/geonode/upload/handlers/tiles3d/tests.py b/geonode/upload/handlers/tiles3d/tests.py new file mode 100755 index 00000000000..4db2b54d6b0 --- /dev/null +++ b/geonode/upload/handlers/tiles3d/tests.py @@ -0,0 +1,468 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +import shutil +from django.test import TestCase +from geonode.upload.handlers.tiles3d.exceptions import Invalid3DTilesException +from geonode.upload.handlers.tiles3d.handler import Tiles3DFileHandler +from django.contrib.auth import get_user_model +from geonode.upload import project_dir +from geonode.upload.orchestrator import orchestrator +from geonode.upload.models import UploadParallelismLimit +from geonode.upload.api.exceptions import UploadParallelismLimitException +from geonode.base.populate_test_data import create_single_dataset +from osgeo import ogr +from geonode.assets.handlers import asset_handler_registry + + +class TestTiles3DFileHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = Tiles3DFileHandler() + cls.valid_3dtile = f"{project_dir}/tests/fixture/3dtilesample/valid_3dtiles.zip" + cls.valid_tileset = f"{project_dir}/tests/fixture/3dtilesample/tileset.json" + cls.valid_tileset_with_region = f"{project_dir}/tests/fixture/3dtilesample/tileset_with_region.json" + cls.invalid_tileset = f"{project_dir}/tests/fixture/3dtilesample/invalid_tileset.json" + cls.invalid_3dtile = f"{project_dir}/tests/fixture/3dtilesample/invalid.zip" + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_3dtile} + cls.valid_files = {"base_file": cls.valid_3dtile} + cls.owner = get_user_model().objects.exclude(username="AnonymousUser").first() + cls.layer = create_single_dataset(name="urban_forestry_street_tree_benefits_epsg_26985", owner=cls.owner) + cls.asset_handler = asset_handler_registry.get_default_handler() + cls.default_bbox = [-180.0, 180.0, -90.0, 90.0, "EPSG:4326"] + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.create_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 3) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_task_list_is_the_expected_one_copy(self): + expected = ( + "start_copy", + "geonode.upload.copy_geonode_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["copy"]), 2) + self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + + def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): + parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") + old_value = parallelism.max_number + try: + UploadParallelismLimit.objects.filter(slug="default_max_parallel_uploads").update(max_number=0) + + with self.assertRaises(UploadParallelismLimitException): + self.handler.is_valid(files=self.valid_files, user=self.user) + + finally: + parallelism.max_number = old_value + parallelism.save() + + def test_is_valid_should_pass_with_valid_3dtiles(self): + self.handler.is_valid(files={"base_file": self.valid_tileset}, user=self.user) + + def test_is_valid_should_raise_exception_if_no_basefile_is_supplied(self): + data = {"base_file": ""} + with self.assertRaises(Invalid3DTilesException) as _exc: + self.handler.is_valid(files=data, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("base file is not provided" in str(_exc.exception.detail)) + + def test_extract_params_from_data(self): + actual, _data = self.handler.extract_params_from_data( + _data={"defaults": '{"title":"title_of_the_cloned_resource"}'}, + action="copy", + ) + + self.assertEqual(actual, {"title": "title_of_the_cloned_resource"}) + + def test_is_valid_should_raise_exception_if_the_3dtiles_is_invalid(self): + data = {"base_file": "/using/double/dot/in/the/name/is/an/error/file.invalid.json"} + with self.assertRaises(Invalid3DTilesException) as _exc: + self.handler.is_valid(files=data, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("Please remove the additional dots in the filename" in str(_exc.exception.detail)) + + def test_is_valid_should_raise_exception_if_the_3dtiles_is_invalid_format(self): + with self.assertRaises(Invalid3DTilesException) as _exc: + self.handler.is_valid(files={"base_file": self.invalid_tileset}, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue( + "The provided 3DTiles is not valid, some of the mandatory keys are missing. Mandatory keys are: 'asset', 'geometricError', 'root'" + in str(_exc.exception.detail) + ) + + def test_validate_should_raise_exception_for_invalid_asset_key(self): + _json = { + "asset": {"invalid_key": ""}, + "geometricError": 1.0, + "root": {"boundingVolume": {"box": []}, "geometricError": 0.0}, + } + _path = "/tmp/tileset.json" + with open(_path, "w") as _f: + _f.write(json.dumps(_json)) + with self.assertRaises(Invalid3DTilesException) as _exc: + self.handler.is_valid(files={"base_file": _path}, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("The mandatory 'version' for the key 'asset' is missing" in str(_exc.exception.detail)) + os.remove(_path) + + def test_validate_should_raise_exception_for_invalid_root_boundingVolume(self): + _json = { + "asset": {"version": "1.1"}, + "geometricError": 1.0, + "root": {"foo": {"box": []}, "geometricError": 0.0}, + } + _path = "/tmp/tileset.json" + with open(_path, "w") as _f: + _f.write(json.dumps(_json)) + with self.assertRaises(Invalid3DTilesException) as _exc: + self.handler.is_valid(files={"base_file": _path}, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("The mandatory 'boundingVolume' for the key 'root' is missing" in str(_exc.exception.detail)) + os.remove(_path) + + def test_validate_should_raise_exception_for_invalid_root_geometricError(self): + _json = { + "asset": {"version": "1.1"}, + "geometricError": 1.0, + "root": {"boundingVolume": {"box": []}, "foo": 0.0}, + } + _path = "/tmp/tileset.json" + with open(_path, "w") as _f: + _f.write(json.dumps(_json)) + with self.assertRaises(Invalid3DTilesException) as _exc: + self.handler.is_valid(files={"base_file": _path}, user=self.user) + + self.assertIsNotNone(_exc) + self.assertTrue("The mandatory 'geometricError' for the key 'root' is missing" in str(_exc.exception.detail)) + os.remove(_path) + + def test_get_ogr2ogr_driver_should_return_the_expected_driver(self): + expected = ogr.GetDriverByName("3dtiles") + actual = self.handler.get_ogr2ogr_driver() + self.assertEqual(type(expected), type(actual)) + + def test_can_handle_should_return_true_for_3dtiles(self): + actual = self.handler.can_handle({"base_file": self.valid_tileset}) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.gpkg"}) + self.assertFalse(actual) + + def test_can_handle_should_return_false_if_no_basefile(self): + actual = self.handler.can_handle({"base_file": ""}) + self.assertFalse(actual) + + def test_supported_file_extension_config(self): + """ + should return the expected value + """ + expected = { + "id": "3dtiles", + "label": "3D Tiles", + "format": "vector", + "ext": ["json"], + "optional": ["xml", "sld"], + } + actual = self.handler.supported_file_extension_config + self.assertDictEqual(actual, expected) + + def test_generate_resource_payload(self): + exec_id = orchestrator.create_execution_request( + user=self.owner, + func_name="funct1", + step="step", + input_params={"files": self.valid_files, "skip_existing_layer": True}, + ) + _exec_obj = orchestrator.get_execution_object(exec_id) + expected = dict( + resource_type="dataset", + subtype="3dtiles", + dirty_state=True, + title="Layer name", + owner=self.owner, + asset="asset", + link_type="uploaded", + extension="3dtiles", + alternate="alternate", + ) + + actual = self.handler.generate_resource_payload("Layer name", "alternate", "asset", _exec_obj, None) + self.assertSetEqual(set(list(actual.keys())), set(list(expected.keys()))) + self.assertDictEqual(actual, expected) + + def test_create_geonode_resource_validate_bbox_with_region(self): + shutil.copy(self.valid_tileset_with_region, "/tmp/tileset.json") + + exec_id, asset = self._generate_execid_asset() + + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=asset, + ) + + # validate bbox + self.assertFalse(resource.bbox == self.default_bbox) + expected = [ + -75.6144410959485, + -75.60974751970046, + 40.040721313841274, + 40.04433990901052, + "EPSG:4326", + ] + self.assertTrue(resource.bbox == expected) + + def test_set_bbox_from_bounding_volume_wit_transform(self): + # https://github.com/geosolutions-it/MapStore2/blob/master/web/client/api/__tests__/ThreeDTiles-test.js#L102-L146 + tilesetjson_file = { + "asset": {"version": "1.1"}, + "geometricError": 1.0, + "root": { + "transform": [ + 96.86356343768793, + 24.848542777253734, + 0, + 0, + -15.986465724980844, + 62.317780594908875, + 76.5566922962899, + 0, + 19.02322243409411, + -74.15554020821229, + 64.3356267137516, + 0, + 1215107.7612304366, + -4736682.902037748, + 4081926.095098698, + 1, + ], + "boundingVolume": { + "box": [0, 0, 0, 7.0955, 0, 0, 0, 3.1405, 0, 0, 0, 5.0375], + }, + }, + } + + with open("/tmp/tileset.json", "w+") as js_file: + js_file.write(json.dumps(tilesetjson_file)) + + exec_id, asset = self._generate_execid_asset() + + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=asset, + ) + self.assertFalse(resource.bbox == self.default_bbox) + + self.assertEqual(resource.bbox_x0, -75.61852101302848) + self.assertEqual(resource.bbox_x1, -75.60566760262047) + self.assertEqual(resource.bbox_y0, 40.03610390613993) + self.assertEqual(resource.bbox_y1, 40.04895731654794) + + os.remove("/tmp/tileset.json") + + def test_set_bbox_from_bounding_volume_without_transform(self): + # https://github.com/geosolutions-it/MapStore2/blob/master/web/client/api/__tests__/ThreeDTiles-test.js#L147-L180 + tilesetjson_file = { + "asset": {"version": "1.1"}, + "geometricError": 1.0, + "root": { + "boundingVolume": { + "box": [ + 0.2524109, + 9.536743e-7, + 4.5, + 16.257824, + 0.0, + 0.0, + 0.0, + -19.717258, + 0.0, + 0.0, + 0.0, + 4.5, + ] + } + }, + } + + with open("/tmp/tileset.json", "w+") as js_file: + js_file.write(json.dumps(tilesetjson_file)) + + exec_id, asset = self._generate_execid_asset() + + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=asset, + ) + self.assertFalse(resource.bbox == self.default_bbox) + + self.assertEqual(resource.bbox_x0, -1.3348442882497923e-05) + self.assertEqual(resource.bbox_x1, 0.0004463052796897286) + self.assertEqual(resource.bbox_y0, 86.81078622278615) + self.assertEqual(resource.bbox_y1, 86.81124587650872) + + os.remove("/tmp/tileset.json") + + def test_set_bbox_from_bounding_volume_sphere_with_transform(self): + # https://github.com/geosolutions-it/MapStore2/blob/master/web/client/api/__tests__/ThreeDTiles-test.js#L102-L146 + tilesetjson_file = { + "asset": {"version": "1.1"}, + "geometricError": 1.0, + "root": { + "transform": [ + 0.968635634376879, + 0.24848542777253735, + 0, + 0, + -0.15986460794399626, + 0.6231776137472074, + 0.7655670897127491, + 0, + 0.190232265775849, + -0.7415555636019701, + 0.6433560687121489, + 0, + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042, + 1, + ], + "boundingVolume": {"sphere": [0, 0, 0, 5]}, + }, + } + + with open("/tmp/tileset.json", "w+") as js_file: + js_file.write(json.dumps(tilesetjson_file)) + + exec_id, asset = self._generate_execid_asset() + + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=asset, + ) + self.assertFalse(resource.bbox == self.default_bbox) + + self.assertAlmostEqual(resource.bbox_x0, -75.61213927392595) + self.assertAlmostEqual(resource.bbox_x1, -75.61204934172301) + self.assertAlmostEqual(resource.bbox_y0, 40.042485645323616) + self.assertAlmostEqual(resource.bbox_y1, 40.042575577526556) + + os.remove("/tmp/tileset.json") + + def test_set_bbox_from_bounding_volume_sphere_without_transform(self): + # https://github.com/geosolutions-it/MapStore2/blob/master/web/client/api/__tests__/ThreeDTiles-test.js#L53C4-L79C8 + tilesetjson_file = { + "asset": {"version": "1.1"}, + "geometricError": 1.0, + "root": {"boundingVolume": {"sphere": [0.2524109, 9.536743e-7, 4.5, 5]}}, + } + + with open("/tmp/tileset.json", "w+") as js_file: + js_file.write(json.dumps(tilesetjson_file)) + + exec_id, asset = self._generate_execid_asset() + + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=asset, + ) + self.assertFalse(resource.bbox == self.default_bbox) + + self.assertEqual(resource.bbox_x0, 0.00017151231693387494) + self.assertEqual(resource.bbox_x1, 0.00026144451987335574) + self.assertEqual(resource.bbox_y0, 86.81097108354597) + self.assertEqual(resource.bbox_y1, 86.8110610157489) + + os.remove("/tmp/tileset.json") + + def test_set_bbox_from_bounding_volume_sphere_with_center_zero_without_transform(self): + # https://github.com/geosolutions-it/MapStore2/blob/master/web/client/api/__tests__/ThreeDTiles-test.js#L53C4-L79C8 + # This test should not extract bbox from boundingVolume sphere with center 0, 0, 0 + tilesetjson_file = { + "asset": {"version": "1.1"}, + "geometricError": 1.0, + "root": {"boundingVolume": {"sphere": [0, 0, 0, 5]}}, + } + + with open("/tmp/tileset.json", "w+") as js_file: + js_file.write(json.dumps(tilesetjson_file)) + + exec_id, asset = self._generate_execid_asset() + + resource = self.handler.create_geonode_resource( + "layername", + "layeralternate", + execution_id=exec_id, + resource_type="ResourceBase", + asset=asset, + ) + self.assertTrue(resource.bbox == self.default_bbox) + + os.remove("/tmp/tileset.json") + + def _generate_execid_asset(self): + exec_id = orchestrator.create_execution_request( + user=self.owner, + func_name="funct1", + step="step", + input_params={ + "files": {"base_file": "/tmp/tileset.json"}, + "skip_existing_layer": True, + }, + ) + asset = self.asset_handler.create( + title="Original", + owner=self.owner, + description=None, + type=str(self.handler), + files=["/tmp/tileset.json"], + clone_files=False, + ) + + return exec_id, asset diff --git a/geonode/upload/handlers/tiles3d/utils.py b/geonode/upload/handlers/tiles3d/utils.py new file mode 100644 index 00000000000..befe7aecc20 --- /dev/null +++ b/geonode/upload/handlers/tiles3d/utils.py @@ -0,0 +1,245 @@ +######################################################################### +# +# Copyright (C) 2024 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 math +import logging +import numpy as np + +logger = logging.getLogger("importer") + + +wgs84OneOverRadii = np.array([1.0 / 6378137.0, 1.0 / 6378137.0, 1.0 / 6356752.3142451793]) +wgs84OneOverRadiiSquared = np.array( + [ + 1.0 / (6378137.0 * 6378137.0), + 1.0 / (6378137.0 * 6378137.0), + 1.0 / (6356752.3142451793 * 6356752.3142451793), + ] +) +wgs84CenterToleranceSquared = 0.1 +CesiumMath_EPSILON12 = 0.000000000001 + + +def fromOrientedBoundingBox(center, halfAxes): + u = halfAxes[:, 0] + v = halfAxes[:, 1] + w = halfAxes[:, 2] + + u = np.add(u, v) + w = np.add(u, w) + + return {"center": center, "radius": np.linalg.norm(u)} + + +def scaleToGeodeticSurface(cartesian, oneOverRadii, oneOverRadiiSquared, centerToleranceSquared): + # https://github.com/CesiumGS/cesium/blob/main/packages/engine/Source/Core/scaleToGeodeticSurface.js#L25 + positionX = cartesian[0] + positionY = cartesian[1] + positionZ = cartesian[2] + + oneOverRadiiX = oneOverRadii[0] + oneOverRadiiY = oneOverRadii[1] + oneOverRadiiZ = oneOverRadii[2] + + x2 = positionX * positionX * oneOverRadiiX * oneOverRadiiX + y2 = positionY * positionY * oneOverRadiiY * oneOverRadiiY + z2 = positionZ * positionZ * oneOverRadiiZ * oneOverRadiiZ + + squaredNorm = x2 + y2 + z2 + ratio = math.sqrt(1.0 / squaredNorm) + + intersection = cartesian * ratio + + if squaredNorm < centerToleranceSquared: + return intersection[:3] if np.isfinite(ratio) else np.NaN + + oneOverRadiiSquaredX = oneOverRadiiSquared[0] + oneOverRadiiSquaredY = oneOverRadiiSquared[1] + oneOverRadiiSquaredZ = oneOverRadiiSquared[2] + + gradient = np.ones(3) + gradient[0] = intersection[0] * oneOverRadiiSquaredX * 2.0 + gradient[1] = intersection[1] * oneOverRadiiSquaredY * 2.0 + gradient[2] = intersection[2] * oneOverRadiiSquaredZ * 2.0 + + _lambda = ((1.0 - ratio) * np.linalg.norm(cartesian)) / (0.5 * np.linalg.norm(gradient)) + correction = 0.0 + + while True: + _lambda -= correction + + xMultiplier = 1.0 / (1.0 + _lambda * oneOverRadiiSquaredX) + yMultiplier = 1.0 / (1.0 + _lambda * oneOverRadiiSquaredY) + zMultiplier = 1.0 / (1.0 + _lambda * oneOverRadiiSquaredZ) + + xMultiplier2 = xMultiplier * xMultiplier + yMultiplier2 = yMultiplier * yMultiplier + zMultiplier2 = zMultiplier * zMultiplier + + xMultiplier3 = xMultiplier2 * xMultiplier + yMultiplier3 = yMultiplier2 * yMultiplier + zMultiplier3 = zMultiplier2 * zMultiplier + + func = x2 * xMultiplier2 + y2 * yMultiplier2 + z2 * zMultiplier2 - 1.0 + + # "denominator" here refers to the use of this expression in the velocity and acceleration + # computations in the sections to follow. + denominator = ( + x2 * xMultiplier3 * oneOverRadiiSquaredX + + y2 * yMultiplier3 * oneOverRadiiSquaredY + + z2 * zMultiplier3 * oneOverRadiiSquaredZ + ) + + derivative = -2.0 * denominator + correction = func / derivative + + if math.fabs(func) > CesiumMath_EPSILON12: + break + + result = np.ones(3) + result[0] = positionX * xMultiplier + result[1] = positionY * yMultiplier + result[2] = positionZ * zMultiplier + + return result + + +def fromCartesian(cartesian): + # https://github.com/CesiumGS/cesium/blob/main/packages/engine/Source/Core/Cartographic.js#L116 + p = scaleToGeodeticSurface( + cartesian, + wgs84OneOverRadii, + wgs84OneOverRadiiSquared, + wgs84CenterToleranceSquared, + ) + + n = p * wgs84OneOverRadiiSquared + n = n / np.linalg.norm(n) + + h = cartesian[:3] - p + longitude = math.atan2(n[1], n[0]) + latitude = math.asin(n[2]) + + height = math.copysign(1, h.dot(cartesian[:3])) * np.linalg.norm(h) + + return {"longitude": longitude, "latitude": latitude, "height": height} + + +def getScale(matrix): + """ + Cesium.Matrix4.getScale() + Extracts the non-uniform scale assuming the matrix is an affine transformation + # https://github.com/CesiumGS/cesium/blob/1.118/packages/engine/Source/Core/Matrix4.js#L1596-L1612 + """ + + # check the type of the matrix + if not isinstance(matrix, np.ndarray): + print("Please define a NumPy array object") + + x = np.linalg.norm([matrix[0][0], matrix[0][1], matrix[0][2]]) + y = np.linalg.norm([matrix[1][0], matrix[1][1], matrix[1][2]]) + z = np.linalg.norm([matrix[2][0], matrix[2][1], matrix[2][2]]) + + result = np.array([x, y, z]) + + return result + + +def box_to_wgs84(box_raw, transform_raw): + box = box_raw + transform_raw = transform_raw or [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] + # transform_raw = transform_raw if transform_raw else range[12] + + transform = np.array( + [ + transform_raw[0:4], + transform_raw[4:8], + transform_raw[8:12], + transform_raw[12:16], + ] + ) + + point = np.array([box[0], box[1], box[2], 1]) + center = point.dot(transform) # Cesium.Matrix4.multiplyByPoint + rotationScale = transform[:3, :3] # Cesium.Matrix4.getMatrix3 + halfAxes = np.multiply( + rotationScale, + np.array( + [ + [box[3], box[4], box[5]], + [box[6], box[7], box[8]], + [box[9], box[10], box[11]], + ] + ), + ) + + sphere = fromOrientedBoundingBox(center, halfAxes) + cartographic = fromCartesian(sphere["center"]) + + lng = math.degrees(cartographic["longitude"]) + lat = math.degrees(cartographic["latitude"]) + + # https://github.com/geosolutions-it/MapStore2/blob/master/web/client/utils/MapUtils.js#L51C16-L51C34 + radiusDegrees = sphere["radius"] / 111194.87428468118 + + return { + "minx": lng - radiusDegrees, + "miny": lat - radiusDegrees, + "maxx": lng + radiusDegrees, + "maxy": lat + radiusDegrees, + } + + +def sphere_to_wgs84(sphere_raw, transform_raw): + + transform_raw = transform_raw or [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] + + transform = np.array( + [ + transform_raw[0:4], + transform_raw[4:8], + transform_raw[8:12], + transform_raw[12:16], + ] + ) + + centerPoint = np.array([sphere_raw[0], sphere_raw[1], sphere_raw[2], 1]) + + radius = sphere_raw[3] + + # Sphere center after the transformation + center = centerPoint.dot(transform) # Cesium.Matrix4.multiplyByPoint + + scale = getScale(transform) + uniformScale = np.max(scale) + radiusDegrees = (radius * uniformScale) / 111194.87428468118 # degrees of one meter + + cartographic = fromCartesian(center) + + if not cartographic: + return None + + lng = math.degrees(cartographic["longitude"]) + lat = math.degrees(cartographic["latitude"]) + + return { + "minx": lng - radiusDegrees, + "miny": lat - radiusDegrees, + "maxx": lng + radiusDegrees, + "maxy": lat + radiusDegrees, + } diff --git a/geonode/upload/handlers/utils.py b/geonode/upload/handlers/utils.py new file mode 100644 index 00000000000..e6ea039d3f2 --- /dev/null +++ b/geonode/upload/handlers/utils.py @@ -0,0 +1,159 @@ +######################################################################### +# +# Copyright (C) 2024 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 enum +import hashlib + +from django.contrib.auth import get_user_model +from geonode.base.models import ResourceBase +from geonode.resource.models import ExecutionRequest +import logging +from dynamic_models.schema import ModelSchemaEditor +from django.utils.module_loading import import_string +from uuid import UUID + +from geonode.upload.publisher import DataPublisher + +logger = logging.getLogger("importer") + + +# TODO this part should be improved when we will drop the legacy upload templates +class UploadSourcesEnum(enum.Enum): + upload = "upload" # used in the default upload flow + resource_file_upload = "resource_file_upload" # source used for the single resource metadata upload + + +STANDARD_TYPE_MAPPING = { + "Integer64": "django.db.models.IntegerField", + "Integer": "django.db.models.IntegerField", + "DateTime": "django.db.models.DateTimeField", + "Date": "django.db.models.DateField", + "Real": "django.db.models.FloatField", + "String": "django.db.models.CharField", + "StringList": "django.db.models.fields.json.JSONField", +} + +GEOM_TYPE_MAPPING = { + "Line String": "django.contrib.gis.db.models.fields.LineStringField", + "Linestring": "django.contrib.gis.db.models.fields.LineStringField", + "3D Line String": "django.contrib.gis.db.models.fields.LineStringField", + "Multi Line String": "django.contrib.gis.db.models.fields.MultiLineStringField", + "Multilinestring": "django.contrib.gis.db.models.fields.MultiLineStringField", + "3D Multi Line String": "django.contrib.gis.db.models.fields.MultiLineStringField", + "Point": "django.contrib.gis.db.models.fields.PointField", + "3D Point": "django.contrib.gis.db.models.fields.PointField", + "Multi Point": "django.contrib.gis.db.models.fields.MultiPointField", + "Multipoint": "django.contrib.gis.db.models.fields.MultiPointField", + "Polygon": "django.contrib.gis.db.models.fields.PolygonField", + "3D Polygon": "django.contrib.gis.db.models.fields.PolygonField", + "3D Multi Point": "django.contrib.gis.db.models.fields.MultiPointField", + "Multi Polygon": "django.contrib.gis.db.models.fields.MultiPolygonField", + "Multipolygon": "django.contrib.gis.db.models.fields.MultiPolygonField", + "3D Multi Polygon": "django.contrib.gis.db.models.fields.MultiPolygonField", +} + + +def should_be_imported(layer: str, user: get_user_model(), **kwargs) -> bool: # type: ignore + """ + If layer_name + user (without the addition of any execution id) + already exists, will apply one of the rule available: + - skip_existing_layer: means that if already exists will be skept + - ovverwrite_layer: means that if already exists will be overridden + - the dynamic model should be recreated + - ogr2ogr should overwrite the layer + - the publisher should republish the resource + - geonode should update it + """ + workspace = DataPublisher(None).workspace + exists = ResourceBase.objects.filter(alternate=f"{workspace.name}:{layer}", owner=user).exists() + + if exists and kwargs.get("skip_existing_layer", False): + return False + + return True + + +def create_alternate(layer_name, execution_id): + """ + Utility to generate the expected alternate for the resource + is alternate = layer_name_ + md5(layer_name + uuid) + """ + _hash = hashlib.md5(f"{layer_name}_{execution_id}".encode("utf-8")).hexdigest() + alternate = f"{layer_name}_{_hash}" + if len(alternate) >= 63: # 63 is the max table lengh in postgres to stay safe, we cut at 12 + return f"{layer_name[:50]}{_hash[:12]}" + return alternate + + +def drop_dynamic_model_schema(schema_model): + if schema_model: + schema = ModelSchemaEditor(initial_model=schema_model.name, db_name="datastore") + try: + schema_model.delete() + schema.drop_table(schema_model.as_model()) + except Exception as e: + logger.warning(e.args[0]) + + +def get_uuid(_list): + for el in _list: + try: + UUID(el) + return el + except Exception: + continue + + +def evaluate_error(celery_task, exc, task_id, args, kwargs, einfo): + """ + Main error function used by the task for the "on_failure" function + """ + from geonode.upload.celery_tasks import orchestrator + + exec_id = orchestrator.get_execution_object(exec_id=get_uuid(args)) + output_params = exec_id.output_params.copy() + + if exec_id.status == ExecutionRequest.STATUS_FAILED: + logger.info("Execution is already in status FAILED") + return + + logger.error(f"Task FAILED with ID: {str(exec_id.exec_id)}, reason: {exc}") + + handler = import_string(exec_id.input_params.get("handler_module_path")) + + # creting the log message + _log = handler.create_error_log(exc, celery_task.name, *args) + + if output_params.get("errors"): + output_params.get("errors").append(_log) + output_params.get("failed_layers", []).append(args[-1] if args else []) + failed = list(set(output_params.get("failed_layers", []))) + output_params["failed_layers"] = failed + else: + output_params = {"errors": [_log], "failed_layers": [args[-1]]} + + celery_task.update_state( + task_id=task_id, + state="FAILURE", + meta={"exec_id": str(exec_id.exec_id), "reason": _log}, + ) + orchestrator.update_execution_request_status(execution_id=str(exec_id.exec_id), output_params=output_params) + + orchestrator.evaluate_execution_progress( + get_uuid(args), _log=str(exc.detail if hasattr(exc, "detail") else exc.args[0]) + ) diff --git a/geonode/upload/handlers/xml/__init__.py b/geonode/upload/handlers/xml/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/handlers/xml/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/handlers/xml/exceptions.py b/geonode/upload/handlers/xml/exceptions.py new file mode 100644 index 00000000000..e4d538f7705 --- /dev/null +++ b/geonode/upload/handlers/xml/exceptions.py @@ -0,0 +1,27 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.exceptions import APIException +from rest_framework import status + + +class InvalidXmlException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The xml provided is invalid" + default_code = "invalid_xml" + category = "importer" diff --git a/geonode/upload/handlers/xml/handler.py b/geonode/upload/handlers/xml/handler.py new file mode 100644 index 00000000000..a6f6340689f --- /dev/null +++ b/geonode/upload/handlers/xml/handler.py @@ -0,0 +1,93 @@ +######################################################################### +# +# Copyright (C) 2024 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 geonode.resource.manager import resource_manager +from geonode.upload.handlers.common.metadata import MetadataFileHandler +from geonode.upload.handlers.xml.exceptions import InvalidXmlException +from owslib.etree import etree as dlxml + +logger = logging.getLogger("importer") + + +class XMLFileHandler(MetadataFileHandler): + """ + Handler to import XML files into GeoNode data db + It must provide the task_lists required to comple the upload + """ + + @property + def supported_file_extension_config(self): + return { + "id": "xml", + "label": "XML Metadata File", + "format": "metadata", + "ext": ["xml"], + "mimeType": ["application/json"], + "needsFiles": [ + "shp", + "prj", + "dbf", + "shx", + "csv", + "tiff", + "zip", + "sld", + "geojson", + ], + } + + @staticmethod + def can_handle(_data) -> bool: + """ + This endpoint will return True or False if with the info provided + the handler is able to handle the file or not + """ + base = _data.get("base_file") + if not base: + return False + return ( + base.endswith(".xml") if isinstance(base, str) else base.name.endswith(".xml") + ) and MetadataFileHandler.can_handle(_data) + + @staticmethod + def is_valid(files, user=None, **kwargs): + """ + Define basic validation steps + """ + # calling base validation checks + try: + with open(files.get("base_file")) as _xml: + dlxml.fromstring(_xml.read().encode()) + except Exception as err: + raise InvalidXmlException(f"Uploaded document is not XML or is invalid: {str(err)}") + return True + + def handle_metadata_resource(self, _exec, dataset, original_handler): + if original_handler.can_handle_xml_file: + original_handler.handle_xml_file(dataset, _exec) + else: + _path = _exec.input_params.get("files", {}).get("xml_file", _exec.input_params.get("base_file", {})) + resource_manager.update( + None, + instance=dataset, + xml_file=_path, + metadata_uploaded=True if _path else False, + vals={"dirty_state": True}, + ) diff --git a/geonode/upload/handlers/xml/serializer.py b/geonode/upload/handlers/xml/serializer.py new file mode 100644 index 00000000000..a28ddd2118a --- /dev/null +++ b/geonode/upload/handlers/xml/serializer.py @@ -0,0 +1,35 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework import serializers +from dynamic_rest.serializers import DynamicModelSerializer + +from geonode.base.models import ResourceBase + + +class MetadataFileSerializer(DynamicModelSerializer): + class Meta: + ref_name = "MetadataFileSerializer" + model = ResourceBase + view_name = "importer_upload" + fields = ("overwrite_existing_layer", "resource_pk", "base_file", "source") + + base_file = serializers.FileField() + overwrite_existing_layer = serializers.BooleanField(required=False, default=True) + resource_pk = serializers.CharField(required=True) + source = serializers.CharField(required=False, default="resource_file_upload") diff --git a/geonode/upload/handlers/xml/tests.py b/geonode/upload/handlers/xml/tests.py new file mode 100644 index 00000000000..af9916a1fc8 --- /dev/null +++ b/geonode/upload/handlers/xml/tests.py @@ -0,0 +1,105 @@ +######################################################################### +# +# Copyright (C) 2024 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 shutil +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import TestCase, override_settings +from geonode.base.populate_test_data import create_single_dataset +from geonode.upload import project_dir +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload.orchestrator import orchestrator +from geonode.upload.handlers.xml.exceptions import InvalidXmlException +from geonode.upload.handlers.xml.handler import XMLFileHandler + + +class TestXMLFileHandler(TestCase): + databases = ("default", "datastore") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.handler = XMLFileHandler() + cls.valid_xml = f"{settings.PROJECT_ROOT}/base/fixtures/test_xml.xml" + cls.invalid_xml = f"{project_dir}/tests/fixture/invalid.gpkg" + + shutil.copy(cls.valid_xml, "/tmp") + cls.user, _ = get_user_model().objects.get_or_create(username="admin") + cls.invalid_files = {"base_file": cls.invalid_xml, "xml_file": cls.invalid_xml} + cls.valid_files = { + "base_file": "/tmp/test_xml.xml", + "xml_file": "/tmp/test_xml.xml", + "source": "resource_file_upload", + } + cls.owner = get_user_model().objects.first() + cls.layer = create_single_dataset(name="extruded_polygon", owner=cls.owner) + + def setUp(self) -> None: + shutil.copy(self.valid_xml, "/tmp") + super().setUp() + + def test_task_list_is_the_expected_one(self): + expected = ( + "start_import", + "geonode.upload.import_resource", + ) + self.assertEqual(len(self.handler.ACTIONS["import"]), 2) + self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + + def test_is_valid_should_raise_exception_if_the_xml_is_invalid(self): + with self.assertRaises(InvalidXmlException) as _exc: + self.handler.is_valid(files=self.invalid_files) + + self.assertIsNotNone(_exc) + self.assertTrue("Uploaded document is not XML or is invalid" in str(_exc.exception.detail)) + + def test_is_valid_should_pass_with_valid_xml(self): + self.handler.is_valid(files=self.valid_files) + + def test_can_handle_should_return_true_for_xml(self): + actual = self.handler.can_handle(self.valid_files) + self.assertTrue(actual) + + def test_can_handle_should_return_false_for_other_files(self): + actual = self.handler.can_handle({"base_file": "random.file"}) + self.assertFalse(actual) + + @override_settings(MEDIA_ROOT="/tmp/") + def test_can_successfully_import_metadata_file(self): + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={ + "files": self.valid_files, + "resource_pk": self.layer.pk, + "skip_existing_layer": True, + "handler_module_path": str(self.handler), + }, + ) + ResourceHandlerInfo.objects.create( + resource=self.layer, + handler_module_path="geonode.upload.handlers.shapefile.handler.ShapeFileHandler", + ) + + self.assertEqual(self.layer.title, "extruded_polygon") + + self.handler.import_resource({}, str(exec_id)) + + self.layer.refresh_from_db() + self.assertEqual(self.layer.title, "test_dataset") diff --git a/geonode/upload/migrations/0040_importer_introduction.py b/geonode/upload/migrations/0040_importer_introduction.py new file mode 100644 index 00000000000..4ef1032610d --- /dev/null +++ b/geonode/upload/migrations/0040_importer_introduction.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.13 on 2022-07-19 07:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("base", "0081_alter_resourcebase_alternate"), + ('upload', '0039_auto_20220506_0833'), + + ] + + operations = [ + migrations.CreateModel( + name="ResourceHandlerInfo", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("handler_module_path", models.CharField(max_length=250)), + ( + "resource", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="base.resourcebase", + ), + ), + ], + ) + ] diff --git a/geonode/upload/migrations/0042_resourcehandlerinfo_kwargs.py b/geonode/upload/migrations/0042_resourcehandlerinfo_kwargs.py new file mode 100644 index 00000000000..b17c0fee336 --- /dev/null +++ b/geonode/upload/migrations/0042_resourcehandlerinfo_kwargs.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.15 on 2022-09-29 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("upload", "0040_importer_introduction"), + ] + + operations = [ + migrations.AddField( + model_name="resourcehandlerinfo", + name="kwargs", + field=models.JSONField( + default=dict, + verbose_name="Storing strictly related information of the handler", + ), + ), + ] diff --git a/geonode/upload/migrations/0043_resourcehandlerinfo_execution_id.py b/geonode/upload/migrations/0043_resourcehandlerinfo_execution_id.py new file mode 100644 index 00000000000..b46a30f34cd --- /dev/null +++ b/geonode/upload/migrations/0043_resourcehandlerinfo_execution_id.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.15 on 2022-10-04 12:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("resource", "0007_alter_executionrequest_action"), + ("upload", "0042_resourcehandlerinfo_kwargs"), + ] + + operations = [ + migrations.AddField( + model_name="resourcehandlerinfo", + name="execution_id", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="resource.executionrequest", + ), + ), + ] diff --git a/geonode/upload/migrations/0044_rename_execution_id_resourcehandlerinfo_execution_request.py b/geonode/upload/migrations/0044_rename_execution_id_resourcehandlerinfo_execution_request.py new file mode 100644 index 00000000000..70db44bff5c --- /dev/null +++ b/geonode/upload/migrations/0044_rename_execution_id_resourcehandlerinfo_execution_request.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.15 on 2022-10-04 13:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("upload", "0043_resourcehandlerinfo_execution_id"), + ] + + operations = [ + migrations.RenameField( + model_name="resourcehandlerinfo", + old_name="execution_id", + new_name="execution_request", + ), + ] diff --git a/geonode/upload/migrations/0045_fixup_dynamic_shema_table_names.py b/geonode/upload/migrations/0045_fixup_dynamic_shema_table_names.py new file mode 100644 index 00000000000..ea27538966b --- /dev/null +++ b/geonode/upload/migrations/0045_fixup_dynamic_shema_table_names.py @@ -0,0 +1,37 @@ +from django.db import migrations +import logging +from django.db import ProgrammingError +from django.db import connections + +logger = logging.getLogger("importer") + + + +def fixup_table_name(apps, schema_editor): + try: + """ + The dynamic model should exists to apply the migration. + In case it does not exists we can skip it + """ + if ( + "dynamic_models_modelschema" + in schema_editor.connection.introspection.table_names() + ): + schema = apps.get_model("dynamic_models", "ModelSchema") + for val in schema.objects.all(): + if val.name != val.db_table_name: + val.db_table_name = val.name + val.save() + except Exception as e: + raise e + + +class Migration(migrations.Migration): + dependencies = [ + ("upload", "0044_rename_execution_id_resourcehandlerinfo_execution_request"), + ("dynamic_models", "0005_auto_20220621_0718"), + ] + + operations = [ + migrations.RunPython(fixup_table_name), + ] diff --git a/geonode/upload/migrations/0046_dataset_migration.py b/geonode/upload/migrations/0046_dataset_migration.py new file mode 100644 index 00000000000..7f9226792de --- /dev/null +++ b/geonode/upload/migrations/0046_dataset_migration.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.15 on 2022-10-04 13:03 + +from django.db import migrations +from geonode.upload.orchestrator import orchestrator +from geonode.layers.models import Dataset + + +def dataset_migration(apps, _): + NewResources = apps.get_model("upload", "ResourceHandlerInfo") + for old_resource in Dataset.objects.exclude( + pk__in=NewResources.objects.values_list("resource_id", flat=True) + ).exclude(subtype__in=["remote", None]): + if hasattr(old_resource, 'files'): + # generating orchestrator expected data file + if not old_resource.files: + if old_resource.is_vector(): + converted_files = [{"base_file": "placeholder.shp"}] + else: + converted_files = [{"base_file": "placeholder.tiff"}] + else: + converted_files = [{"base_file": x} for x in old_resource.files] + # try to get the handler for the file of the old resource + # when is found, we can exit + handler_to_use = None + for _input in converted_files: + handler = orchestrator.get_handler(_input) + if handler is not None: + handler_to_use = handler + break + handler_to_use.create_resourcehandlerinfo( + handler_module_path=str(handler_to_use), + resource=old_resource, + execution_id=None, + kwargs={"is_legacy": True}, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("upload", "0045_fixup_dynamic_shema_table_names"), + ] + + operations = [ + migrations.RunPython(dataset_migration), + ] diff --git a/geonode/upload/migrations/0047_alter_resourcehandlerinfo_id_and_more.py b/geonode/upload/migrations/0047_alter_resourcehandlerinfo_id_and_more.py new file mode 100644 index 00000000000..df3b3bc9705 --- /dev/null +++ b/geonode/upload/migrations/0047_alter_resourcehandlerinfo_id_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.9 on 2024-06-24 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("upload", "0046_dataset_migration"), + ] + + operations = [ + migrations.AlterField( + model_name="resourcehandlerinfo", + name="id", + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + migrations.AlterField( + model_name="uploadparallelismlimit", + name="max_number", + field=models.PositiveSmallIntegerField( + default=5, help_text="The maximum number of parallel uploads (0 to 32767)." + ), + ), + migrations.DeleteModel( + name="Upload", + ), + ] diff --git a/geonode/upload/migrations/0048_alter_resourcehandlerinfo_id.py b/geonode/upload/migrations/0048_alter_resourcehandlerinfo_id.py new file mode 100644 index 00000000000..9931b60408d --- /dev/null +++ b/geonode/upload/migrations/0048_alter_resourcehandlerinfo_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.9 on 2024-09-06 14:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("upload", "0047_alter_resourcehandlerinfo_id_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="resourcehandlerinfo", + name="id", + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ] diff --git a/geonode/upload/migrations/0049_move_data_from_importer_to_upload.py b/geonode/upload/migrations/0049_move_data_from_importer_to_upload.py new file mode 100644 index 00000000000..349d6e5c547 --- /dev/null +++ b/geonode/upload/migrations/0049_move_data_from_importer_to_upload.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.9 on 2024-09-06 14:46 + +from django.db import migrations, models +from django.db import connection +from geonode.upload.models import ResourceHandlerInfo + +def forwards_func(apps, schema_editor): + SQL = """ + INSERT INTO + upload_resourcehandlerinfo (handler_module_path, + resource_id, + kwargs, + execution_request_id) + SELECT + handler_module_path, + resource_id, + kwargs, + execution_request_id + FROM + importer_resourcehandlerinfo ir; + """ + + # checking if the importer table is present. Is false if is a new installation + + names = connection.introspection.table_names() + if 'importer_resourcehandlerinfo' in names: + with connection.cursor() as cursor: + cursor.execute(SQL) + + for row in ResourceHandlerInfo.objects.iterator(): + if 'importer.handlers' in row.handler_module_path: + row.handler_module_path = row.handler_module_path.replace("importer.handlers", "geonode.upload.handlers") + row.save() + + # truncate old TABLE + SQL = """ + DROP TABLE importer_resourcehandlerinfo + """ + names = connection.introspection.table_names() + if 'importer_resourcehandlerinfo' in names: + with connection.cursor() as cursor: + cursor.execute(SQL) + +class Migration(migrations.Migration): + + dependencies = [ + ("upload", "0048_alter_resourcehandlerinfo_id"), + ] + + operations = [ + migrations.RunPython(forwards_func) + ] diff --git a/geonode/upload/models.py b/geonode/upload/models.py index 3bfb8584a4c..425def619ca 100644 --- a/geonode/upload/models.py +++ b/geonode/upload/models.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2024 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 @@ -16,57 +16,21 @@ # along with this program. If not, see . # ######################################################################### - -import os -import base64 -import pickle import logging -from gsimporter.api import NotFound -from django.utils.timezone import now - from django.db import models -from django.urls import reverse -from django.conf import settings +from django.db.models.signals import pre_delete +from django.dispatch import receiver +from geonode.layers.models import Dataset +from geonode.base.models import ResourceBase +from django.utils.translation import gettext_lazy as _ +from geonode.resource.models import ExecutionRequest from django.core.validators import MinLengthValidator +from django.conf import settings from django.template.defaultfilters import filesizeformat -from django.utils.translation import gettext_lazy as _ - -from geonode import GeoNodeException -from geonode.base import enumerations -from geonode.base.models import ResourceBase -from geonode.storage.manager import storage_manager -from geonode.geoserver.helpers import gs_uploader, ogc_server_settings -logger = logging.getLogger(__name__) - -class UploadManager(models.Manager): - def __init__(self): - models.Manager.__init__(self) - - def invalidate_from_session(self, upload_session): - return self.filter(user=upload_session.user, import_id=upload_session.import_session.id).update( - state=enumerations.STATE_INVALID - ) - - def update_from_session(self, upload_session, resource: ResourceBase = None): - _upload = None - if resource: - try: - _upload = self.get(resource=resource) - except Upload.DoesNotExist: - _upload = None - if not _upload: - _upload = self.filter(user=upload_session.user, name=upload_session.name).order_by("-date").first() - if _upload: - return _upload.update_from_session(upload_session, resource=resource) - return None - - def get_incomplete_uploads(self, user): - return ( - self.filter(user=user).exclude(state=enumerations.STATE_PROCESSED).exclude(state=enumerations.STATE_WAITING) - ) +logger = logging.getLogger("importer") class UploadSizeLimitManager(models.Manager): @@ -97,159 +61,6 @@ def create_default_limit(self): return default_limit -class Upload(models.Model): - objects = UploadManager() - - import_id = models.BigIntegerField(null=True) - user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL) - state = models.CharField(max_length=16) - create_date = models.DateTimeField("create_date", default=now) - date = models.DateTimeField("date", default=now) - resource = models.ForeignKey(ResourceBase, null=True, on_delete=models.SET_NULL) - upload_dir = models.TextField(null=True) - store_spatial_files = models.BooleanField(default=True) - name = models.CharField(max_length=64, null=False, blank=False) - complete = models.BooleanField(default=False) - # hold our serialized session object - session = models.TextField(null=True, blank=True) - # hold a dict of any intermediate Dataset metadata - not used for now - metadata = models.TextField(null=True) - - mosaic = models.BooleanField(default=False) - append_to_mosaic_opts = models.BooleanField(default=False) - append_to_mosaic_name = models.CharField(max_length=128, null=True) - - mosaic_time_regex = models.CharField(max_length=128, null=True) - mosaic_time_value = models.CharField(max_length=128, null=True) - - mosaic_elev_regex = models.CharField(max_length=128, null=True) - mosaic_elev_value = models.CharField(max_length=128, null=True) - - resume_url = models.CharField(max_length=256, null=True, blank=True) - - class Meta: - ordering = ["-date"] - - @property - def get_session(self): - if self.session: - return pickle.loads(base64.decodebytes(self.session.encode("UTF-8"))) - - def update_from_session(self, upload_session, resource: ResourceBase = None): - self.session = base64.encodebytes(pickle.dumps(upload_session)).decode("UTF-8") - self.import_id = upload_session.import_session.id - self.name = upload_session.name - self.user = upload_session.user - self.date = now() - - if not self.upload_dir: - self.upload_dir = os.path.dirname(upload_session.base_file) - - if resource: - if not self.resource: - if not isinstance(resource, ResourceBase) and hasattr(resource, "resourcebase_ptr"): - self.resource = resource.resourcebase_ptr - elif not isinstance(resource, ResourceBase): - raise GeoNodeException("Invalid resource uploaded, plase select one of the available") - else: - self.resource = resource - - if upload_session.base_file and len(self.resource.files) == 0: - uploaded_files = upload_session.base_file[0] - aux_files = uploaded_files.auxillary_files - sld_files = uploaded_files.sld_files - xml_files = uploaded_files.xml_files - - if self.store_spatial_files and self.resource and not self.resource.files: - files_to_upload = aux_files + sld_files + xml_files + [uploaded_files.base_file] - if len(files_to_upload): - ResourceBase.objects.upload_files(resource_id=self.resource.id, files=files_to_upload) - self.resource.refresh_from_db() - - if self.resource: - if self.resource.processed: - self.state = enumerations.STATE_PROCESSED - else: - self.state = enumerations.STATE_RUNNING - elif self.state in (enumerations.STATE_READY, enumerations.STATE_PENDING): - self.state = upload_session.import_session.state - if self.state == enumerations.STATE_COMPLETE: - self.complete = True - - self.save() - return self.get_session - - @property - def progress(self): - if self.state in (enumerations.STATE_READY, enumerations.STATE_INVALID, enumerations.STATE_INCOMPLETE): - return 0.0 - elif self.state == enumerations.STATE_PENDING: - return 33.0 - elif self.state == enumerations.STATE_WAITING: - return 50.0 - elif self.state == enumerations.STATE_PROCESSED: - if (self.resource and self.resource.processed) or self.complete: - return 100.0 - return 80.0 - elif self.state in (enumerations.STATE_COMPLETE, enumerations.STATE_RUNNING): - if self.resource and self.resource.state == enumerations.STATE_PROCESSED: - self.state = enumerations.STATE_PROCESSED - self.save() - return 90.0 - return 80.0 - - def set_resume_url(self, resume_url): - if self.resume_url != resume_url: - self.resume_url = resume_url - Upload.objects.filter(id=self.id).update(resume_url=resume_url) - - def get_resume_url(self): - if self.state == enumerations.STATE_WAITING and self.import_id: - return self.resume_url - return None - - def get_delete_url(self): - if self.state != enumerations.STATE_PROCESSED: - return reverse("data_upload_delete", args=[self.id]) - return None - - def get_import_url(self): - session = None - try: - if not self.import_id: - raise NotFound - session = self.get_session.import_session - if not session or session.state != enumerations.STATE_COMPLETE: - session = gs_uploader.get_session(self.import_id) - except (NotFound, Exception): - if not session and self.state not in (enumerations.STATE_COMPLETE, enumerations.STATE_PROCESSED): - logger.warning(f"Import session was not found for upload with ID: {self.pk}") - if session and self.state != enumerations.STATE_INVALID: - return f"{ogc_server_settings.LOCATION}rest/imports/{session.id}" - else: - return None - - def get_detail_url(self): - if self.resource and self.resource.processed: - return getattr(self.resource, "detail_url", None) - else: - return None - - def delete(self, *args, **kwargs): - super().delete(*args, **kwargs) - storage_manager.rmtree(self.upload_dir, ignore_errors=True) - - def set_processing_state(self, state): - if self.state != state: - self.state = state - Upload.objects.filter(id=self.id).update(state=state) - if self.resource: - self.resource.set_processing_state(state) - - def __str__(self): - return f"Upload [{self.pk}] gs{self.import_id} - {self.name}, {self.user}" - - class UploadSizeLimit(models.Model): objects = UploadSizeLimitManager() @@ -310,3 +121,31 @@ def __str__(self): class Meta: ordering = ("slug",) + + +@receiver(pre_delete, sender=Dataset) +def delete_dynamic_model(instance, sender, **kwargs): + """ + Delete the dynamic relation and the geoserver layer + """ + from geonode.upload.orchestrator import orchestrator + + try: + if instance.resourcehandlerinfo_set.exists(): + handler_module_path = instance.resourcehandlerinfo_set.first().handler_module_path + handler = orchestrator.load_handler(handler_module_path) + handler.delete_resource(instance) + # Removing Field Schema + except Exception as e: + logger.error(f"Error during deletion instance deletion: {e}") + + +class ResourceHandlerInfo(models.Model): + """ + Here we save the relation between the geonode resource created and the handler that created that resource + """ + + resource = models.ForeignKey(ResourceBase, blank=False, null=False, on_delete=models.CASCADE) + handler_module_path = models.CharField(max_length=250, blank=False, null=False) + execution_request = models.ForeignKey(ExecutionRequest, null=True, default=None, on_delete=models.SET_NULL) + kwargs = models.JSONField(verbose_name="Storing strictly related information of the handler", default=dict) diff --git a/geonode/upload/orchestrator.py b/geonode/upload/orchestrator.py new file mode 100644 index 00000000000..1611e4f6d91 --- /dev/null +++ b/geonode/upload/orchestrator.py @@ -0,0 +1,341 @@ +######################################################################### +# +# Copyright (C) 2024 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 ast +import logging +from typing import Optional +from uuid import UUID + +from celery import states +from django.contrib.auth import get_user_model +from django.db.models import Q +from django.utils import timezone +from django.utils.module_loading import import_string +from django_celery_results.models import TaskResult +from geonode.resource.models import ExecutionRequest +from rest_framework import serializers + +from geonode.upload.api.exceptions import ImportException +from geonode.upload.api.serializer import ImporterSerializer, OverwriteImporterSerializer +from geonode.upload.celery_app import importer_app +from geonode.upload.handlers.base import BaseHandler +from geonode.upload.utils import error_handler + +logger = logging.getLogger("importer") + + +class ImportOrchestrator: + """' + Main import object. Is responsible to handle all the execution steps + Using the ExecutionRequest object, will extrapolate the information and + it call the next step of the import pipeline + Params: + + """ + + def get_handler(self, _data) -> Optional[BaseHandler]: + """ + If is part of the supported format, return the handler which can handle the import + otherwise return None + """ + for handler in BaseHandler.get_registry(): + if handler.can_handle(_data): + return handler() + logger.error("Handler not found") + return None + + def get_serializer(self, _data) -> serializers.Serializer: + for handler in BaseHandler.get_registry(): + _serializer = handler.has_serializer(_data) + if _serializer: + return _serializer + logger.info("specific serializer not found, fallback on the default one") + is_overwrite_flow = _data.get("overwrite_existing_layer", False) + if isinstance(is_overwrite_flow, str): + is_overwrite_flow = ast.literal_eval(is_overwrite_flow.title()) + return OverwriteImporterSerializer if is_overwrite_flow else ImporterSerializer + + def load_handler(self, module_path): + try: + return import_string(module_path) + except Exception: + raise ImportException(detail=f"The handler is not available: {module_path}") + + def load_handler_by_id(self, handler_id): + for handler in BaseHandler.get_registry(): + if handler().id == handler_id: + return handler + logger.error("Handler not found") + return None + + def get_execution_object(self, exec_id): + """ + Returns the ExecutionRequest object with the detail about the + current execution + """ + req = ExecutionRequest.objects.filter(exec_id=exec_id) + if not req.exists(): + raise ImportException("The selected UUID does not exists") + return req.first() + + def perform_next_step( + self, + execution_id: str, + action: str, + handler_module_path: str, + step: str = None, + layer_name: str = None, + alternate: str = None, + **kwargs, + ) -> None: + """ + It takes the executionRequest detail to extract which was the last step + and take from the task_lists provided by the ResourceType handler + which will be the following step. if empty a None is returned, otherwise + in async the next step is called + """ + try: + _exec_obj = self.get_execution_object(str(execution_id)) + if step is None: + step = _exec_obj.step + + # retrieve the task list for the resource_type + tasks = self.load_handler(handler_module_path).get_task_list(action=action) + # getting the index + if not tasks: + raise StopIteration("Task lists is completed") + _index = tasks.index(step) + 1 + if _index == 1: + """ + Means that the first task is available and we set the executions state as running + So is updated only at the beginning keeping it in a consistent state + """ + self.update_execution_request_status( + execution_id=str(_exec_obj.exec_id), + status=ExecutionRequest.STATUS_RUNNING, + ) + # finding in the task_list the last step done + remaining_tasks = tasks[_index:] if not _index >= len(tasks) else [] + if not remaining_tasks: + # The list of task is empty, it means that the process is finished + self.evaluate_execution_progress(execution_id, handler_module_path=handler_module_path) + return + # getting the next step to perform + next_step = next(iter(remaining_tasks)) + # calling the next step for the resource + + # defining the tasks parameter for the step + task_params = (str(execution_id), handler_module_path, action) + logger.info(f"STARTING NEXT STEP {next_step}") + + if layer_name and alternate: + # if the layer and alternate is provided, means that we are executing the step specifically for a layer + # so we add this information to the task_parameters to be sent to the next step + logger.info(f"STARTING NEXT STEP {next_step} for resource: {layer_name}, alternate {alternate}") + + """ + If layer name and alternate are provided, are sent as an argument + for the next task step + """ + task_params = ( + str(execution_id), + next_step, + layer_name, + alternate, + handler_module_path, + action, + ) + + # continuing to the next step + importer_app.tasks.get(next_step).apply_async(task_params, kwargs) + return execution_id + + except StopIteration: + # means that the expected list of steps is completed + logger.info("The whole list of tasks has been processed") + self.set_as_completed(execution_id) + return + except Exception as e: + self.set_as_failed(execution_id, reason=error_handler(e, execution_id)) + raise e + + def set_as_failed(self, execution_id, reason=None, delete_file=True): + """ + Utility method to set the ExecutionRequest object to fail + """ + self.update_execution_request_status( + execution_id=str(execution_id), + status=ExecutionRequest.STATUS_FAILED, + finished=timezone.now(), + last_updated=timezone.now(), + log=reason, + ) + # delete + if delete_file: + exec_obj = self.get_execution_object(execution_id) + # cleanup asset in case of fail + if exec_obj.input_params.get("asset_module_path", None): + asset_handler = import_string(exec_obj.input_params["asset_module_path"]) + asset = asset_handler.objects.filter(pk=exec_obj.input_params["asset_id"]) + if asset.exists(): + asset.first().delete() + + def set_as_partially_failed(self, execution_id, reason=None): + """ + Utility method to set the ExecutionRequest object to partially failed + """ + self.update_execution_request_status( + execution_id=str(execution_id), + status=ExecutionRequest.STATUS_FAILED, + finished=timezone.now(), + last_updated=timezone.now(), + log=f"The execution is completed, but the following layers are not imported: \n {', '.join(reason)}. Check the logs for additional infos", + ) + + def set_as_completed(self, execution_id): + """ + Utility method to set the ExecutionRequest object to fail + """ + self.update_execution_request_status( + execution_id=str(execution_id), + status=ExecutionRequest.STATUS_FINISHED, + finished=timezone.now(), + last_updated=timezone.now(), + ) + + def evaluate_execution_progress(self, execution_id, _log=None, handler_module_path=None): + from geonode.upload.models import ResourceHandlerInfo + + """ + The execution id is a mandatory argument for the task + We use that to filter out all the task execution that are still in progress. + if any is failed, we raise it. + """ + + _exec = self.get_execution_object(execution_id) + expected_dataset = _exec.input_params.get("total_layers", 0) + actual_dataset = ResourceHandlerInfo.objects.filter(execution_request=_exec).count() + is_last_dataset = actual_dataset >= expected_dataset + execution_id = str(execution_id) # force it as string to be sure + lower_exec_id = execution_id.replace("-", "_").lower() + exec_result = TaskResult.objects.filter( + Q(task_args__icontains=lower_exec_id) + | Q(task_kwargs__icontains=lower_exec_id) + | Q(result__icontains=lower_exec_id) + | Q(task_args__icontains=execution_id) + | Q(task_kwargs__icontains=execution_id) + | Q(result__icontains=execution_id) + ) + _has_data = ResourceHandlerInfo.objects.filter(execution_request__exec_id=execution_id).exists() + + # .all() is needed since we want to have the last status on the DB without take in consideration the cache + if exec_result.all().exclude(Q(status=states.SUCCESS) | Q(status=states.FAILURE)).exists(): + self._evaluate_last_dataset(is_last_dataset, _log, execution_id, handler_module_path) + elif exec_result.all().filter(status=states.FAILURE).exists(): + """ + Should set it fail if all the execution are done and at least 1 is failed + """ + # failed = [x.task_id for x in exec_result.filter(status=states.FAILURE)] + # _log_message = f"For the execution ID {execution_id} The following celery task are failed: {failed}" + if _has_data: + log = list(set(self.get_execution_object(execution_id).output_params.get("failed_layers", ["Unknown"]))) + logger.error(log) + self.set_as_partially_failed(execution_id=execution_id, reason=log) + self._last_step(execution_id, handler_module_path) + + elif is_last_dataset: + self.set_as_failed(execution_id=execution_id, reason=_log) + elif expected_dataset == 1 and not _has_data: + self.set_as_failed(execution_id=execution_id, reason=_log) + else: + self._evaluate_last_dataset(is_last_dataset, _log, execution_id, handler_module_path) + + def _evaluate_last_dataset(self, is_last_dataset, _log, execution_id, handler_module_path): + if is_last_dataset: + if _log and "ErrorDetail" in _log: + self.set_as_failed(execution_id=execution_id, reason=_log) + else: + logger.info(f"Execution with ID {execution_id} is completed. All tasks are done") + self._last_step(execution_id, handler_module_path) + self.set_as_completed(execution_id) + else: + logger.info(f"Execution progress with id {execution_id} is not finished yet, continuing") + return + + def create_execution_request( + self, + user: get_user_model, + func_name: str, + step: str, + input_params: dict = {}, + resource=None, + action=None, + name=None, + source=None, + asset_module_path=None, + ) -> UUID: + """ + Create an execution request for the user. Return the UUID of the request + """ + execution = ExecutionRequest.objects.create( + user=user, + geonode_resource=resource, + func_name=func_name, + step=step, + input_params=input_params, + action=action, + name=name, + source=source, + ) + return execution.exec_id + + def update_execution_request_status( + self, + execution_id, + status=None, + celery_task_request=None, + **kwargs, + ): + """ + Update the execution request status and also the legacy upload status if the + feature toggle is enabled + """ + if status is not None: + kwargs["status"] = status + + ExecutionRequest.objects.filter(exec_id=execution_id).update(**kwargs) + + if celery_task_request: + TaskResult.objects.filter(task_id=celery_task_request.id).update(task_args=celery_task_request.args) + + def update_execution_request_obj(self, _exec_obj, payload): + ExecutionRequest.objects.filter(pk=_exec_obj.pk).update(**payload) + _exec_obj.refresh_from_db() + return _exec_obj + + def _last_step(self, execution_id, handler_module_path): + """ + Last hookable step for each handler before mark the execution as completed + To overwrite this, please hook the method perform_last_step from the Handler + """ + if not handler_module_path: + return + return self.load_handler(handler_module_path).perform_last_step(execution_id) + + +orchestrator = ImportOrchestrator() diff --git a/geonode/upload/publisher.py b/geonode/upload/publisher.py new file mode 100644 index 00000000000..0c7d8234c99 --- /dev/null +++ b/geonode/upload/publisher.py @@ -0,0 +1,214 @@ +######################################################################### +# +# Copyright (C) 2024 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 +import os +from typing import List + +from geonode import settings +from geonode.geoserver.helpers import create_geoserver_db_featurestore +from geoserver.catalog import Catalog +from geonode.utils import OGC_Servers_Handler +from django.utils.module_loading import import_string +from geoserver.support import build_url +from geoserver.catalog import FailedRequestError +from geonode.upload.api.exceptions import PublishResourceException + + +logger = logging.getLogger("importer") + + +class DataPublisher: + """ + Given a list of resources, will publish them on GeoServer + """ + + def __init__(self, handler_module_path) -> None: + ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"] + + _user, _password = ogc_server_settings.credentials + + self.cat = Catalog(service_url=ogc_server_settings.rest, username=_user, password=_password) + self.workspace = self._get_default_workspace(create=True) + + self.store = None + + if handler_module_path is not None: + self.handler = import_string(handler_module_path)() + + def extract_resource_to_publish(self, files: dict, action: str, layer_name, alternate=None, **kwargs): + """ + Will try to extract the layers name from the original file + this is needed since we have to publish the resources + on geoserver by name: + expected output: + [ + {'name': 'layer_name', 'crs': 'EPSG:25832'} + ] + """ + + return self.handler.extract_resource_to_publish(files, action, layer_name, alternate, **kwargs) + + def get_resource(self, resource_name, return_bool=True) -> bool: + self.get_or_create_store(default=resource_name) + _res = self.cat.get_resource(resource_name, store=self.store, workspace=self.workspace) + if return_bool: + return True if _res else False + return _res + + def publish_resources(self, resources: List[str]): + """ + Given a list of strings (which rappresent the table on geoserver) + Will publish the resorces on geoserver + """ + self.get_or_create_store(default=resources[0]["name"]) + result = self.handler.publish_resources( + resources=resources, + catalog=self.cat, + store=self.store, + workspace=self.workspace, + ) + self.sanity_checks(resources) + return result + + def overwrite_resources(self, resources: List[str]): + """ + We dont need to do anything for now. The data is replaced via ogr2ogr + """ + for _resource in resources: + self.get_or_create_store(default=_resource["name"]) + result = self.handler.overwrite_geoserver_resource( + resource=_resource, + catalog=self.cat, + store=self.store, + workspace=self.workspace, + ) + self.sanity_checks(resources) + return result + + def delete_resource(self, resource_name): + layer = self.get_resource(resource_name, return_bool=False) + if layer: + self.cat.delete(layer, purge="all", recurse=True) + store = self.cat.get_store( + resource_name.split(":")[-1], + workspace=os.getenv("DEFAULT_WORKSPACE", os.getenv("CASCADE_WORKSPACE", "geonode")), + ) + if not store: + store = self.cat.get_store( + resource_name, + workspace=os.getenv("DEFAULT_WORKSPACE", os.getenv("CASCADE_WORKSPACE", "geonode")), + ) + if store: + self.cat.delete(store, purge="all", recurse=True) + + def get_or_create_store(self, default=None): + """ + Evaluate if the store exists. if not is created + """ + store_name, to_be_created = self.handler.get_geoserver_store_name(default=default) + + if self.store and self.store.name == store_name: + # if we already initialize the store, we can skip the checks + return self.store + + if store_name is not None and not to_be_created: + # If the store name is provided by the handler, we retrieve the store + # from geoserver. This is usually used for raster layers + # for raster we dont want to create the store upfront since the pulishing + # is going to create it + self.store = self.cat.get_store(name=store_name, workspace=self.workspace) + return + + self.store = self.cat.get_store(name=store_name, workspace=self.workspace) + if not self.store: + logger.warning(f"The store does not exists: {store_name} creating...") + self.store = create_geoserver_db_featurestore(store_name=store_name, workspace=self.workspace.name) + + def publish_geoserver_view(self, layer_name, crs, view_name, sql=None, geometry=None): + """ + Let the handler create a geoserver view given the input parameters + """ + self.get_or_create_store(default=layer_name) + + return self.handler.publish_geoserver_view( + catalog=self.cat, + workspace=self.workspace, + datastore=self.store, + layer_name=layer_name, + crs=crs, + view_name=view_name, + sql=sql, + geometry=geometry, + ) + + def sanity_checks(self, resources): + """ + Will evaluate if the SRID is correctly created + For each resource. This is a quick test to be sure + that the resource is correctly set/created + """ + + for _resource in resources: + possible_layer_name = [ + _resource.get("name"), + _resource.get("name").split(":")[-1], + f"{self.workspace.name}:{_resource.get('name')}", + ] + res = list( + filter( + None, + (self.cat.get_resource(x, workspace=self.workspace) for x in possible_layer_name), + ) + ) + if not res or (res and not res[0].projection): + raise PublishResourceException( + f"The SRID for the resource {_resource} is not correctly set, Please check Geoserver logs" + ) + + def _get_default_workspace(self, create=True): + """Return the default geoserver workspace + The workspace can be created it if needed. + """ + name = getattr(settings, "DEFAULT_WORKSPACE", "geonode") + workspace = self.cat.get_workspace(name) + if workspace is None and create: + uri = f"http://www.geonode.org/{name}" + workspace = self.cat.create_workspace(name, uri) + return workspace + + def recalculate_geoserver_featuretype(self, dataset): + resp = self.cat.http_request( + build_url( + self.cat.service_url, + [ + "workspaces", + dataset.workspace, + "datastores", + os.environ.get("GEONODE_GEODATABASE", "geonode_data"), + "featuretypes", + dataset.alternate.split(":")[-1] + ".xml", + ], + {"recalculate": "nativebbox,latlonbbox"}, + ), + data="true", + method="PUT", + headers={"Content-Type": "application/xml"}, + ) + if resp.status_code not in (200, 201, 202): + raise FailedRequestError("Failed to recalculate featuretype") diff --git a/geonode/upload/settings.py b/geonode/upload/settings.py new file mode 100644 index 00000000000..1496f62a0a2 --- /dev/null +++ b/geonode/upload/settings.py @@ -0,0 +1,41 @@ +######################################################################### +# +# Copyright (C) 2024 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 os + +""" +main settings to handle the celery rate +""" +IMPORTER_GLOBAL_RATE_LIMIT = os.getenv("IMPORTER_GLOBAL_RATE_LIMIT", 5) +IMPORTER_PUBLISHING_RATE_LIMIT = os.getenv("IMPORTER_PUBLISHING_RATE_LIMIT", 5) +IMPORTER_RESOURCE_CREATION_RATE_LIMIT = os.getenv("IMPORTER_RESOURCE_CREATION_RATE_LIMIT", 10) +IMPORTER_RESOURCE_COPY_RATE_LIMIT = os.getenv("IMPORTER_RESOURCE_COPY_RATE_LIMIT", 10) + +SYSTEM_HANDLERS = [ + "geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + "geonode.upload.handlers.geojson.handler.GeoJsonFileHandler", + "geonode.upload.handlers.shapefile.handler.ShapeFileHandler", + "geonode.upload.handlers.kml.handler.KMLFileHandler", + "geonode.upload.handlers.csv.handler.CSVFileHandler", + "geonode.upload.handlers.geotiff.handler.GeoTiffFileHandler", + "geonode.upload.handlers.xml.handler.XMLFileHandler", + "geonode.upload.handlers.sld.handler.SLDFileHandler", + "geonode.upload.handlers.tiles3d.handler.Tiles3DFileHandler", + "geonode.upload.handlers.remote.tiles3d.RemoteTiles3DResourceHandler", + "geonode.upload.handlers.remote.wms.RemoteWMSResourceHandler", +] diff --git a/geonode/upload/tasks.py b/geonode/upload/tasks.py index 9f00d9a0030..02461693870 100644 --- a/geonode/upload/tasks.py +++ b/geonode/upload/tasks.py @@ -19,14 +19,11 @@ from datetime import datetime import logging -from django.conf import settings from django.utils.timezone import timedelta from geonode.celery_app import app -logger = logging.getLogger(__name__) - -UPLOAD_SESSION_EXPIRY_HOURS = getattr(settings, "UPLOAD_SESSION_EXPIRY_HOURS", 24) +logger = logging.getLogger("importer") @app.task(bind=False, acks_late=False, queue="clery_cleanup", ignore_result=True) diff --git a/geonode/upload/templates/__init__.py b/geonode/upload/templates/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/templates/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/templates/upload/dataset_upload_base.html b/geonode/upload/templates/upload/dataset_upload_base.html deleted file mode 100644 index 1f5b4f37fb2..00000000000 --- a/geonode/upload/templates/upload/dataset_upload_base.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "geonode_base.html" %} -{% load i18n %} -{% load base_tags %} -{% load client_lib_tags %} - -{% block head %} -{{ block.super }} -{% endblock %} - -{% block title %} {{ block.super }} {% endblock %} - -{% block body_class %}data{% endblock %} - -{% block body_outer %} - -
- {% block body %}{% endblock body %} - {% block sidebar %}{% endblock sidebar %} -
-{% endblock body_outer %} diff --git a/geonode/upload/templates/upload/dataset_upload_crs.html b/geonode/upload/templates/upload/dataset_upload_crs.html deleted file mode 100644 index 88f789b77bb..00000000000 --- a/geonode/upload/templates/upload/dataset_upload_crs.html +++ /dev/null @@ -1,233 +0,0 @@ -{% extends "upload/dataset_upload_base.html" %} -{% load i18n %} -{% load static %} -{% load dataset_tags %} -{% load client_lib_tags %} - -{% block body_class %}data data-list upload{% endblock %} - -{% block title %} {% trans "Upload Dataset Step: Set SRS" %} - {{ block.super }} {% endblock %} - -{% block head %} -{{ block.super }} -{% endblock %} - -{% block body %} -
-

{% trans "Provide CRS for " %} "{{ dataset_name }}"

- -
-

{% trans "Coordinate Reference System" %}

- {% csrf_token %} - {% if native_crs %} -

- {% blocktrans %} - A coordinate reference system for this dataset could not be determined. - Locate or enter the appropriate ESPG code for this dataset below. - One way to do this is do visit: - prj2epsg - and enter the following: - {% endblocktrans %} -

-
-            {% if native_crs %}{{ native_crs }}{% else %}{% trans "Native CRS could not be found!" %}{% endif %}
-            
- {% endif %} -
- - - -

{% blocktrans %}Source SRS EPSG Code is mandatory and represents the native data Spatial Reference System. -

- This must be coherent with the Geometry values (lon/lat coordinates as an instance) stored on the geospatial dataset. -

- If not specified on the geospatial data itself, it must be manually declared by the operator. -

- More information is provided at the bottom of the page in the "Additional Help" sections. - {% endblocktrans %}

-
-
- - {% trans "Cancel" %}   - -
- -
-
{% trans "Advanced Options" %}:
- -
- -
-
- -   {% trans "leave it empty to use only Source SRS" %} -
-
- - -
-
-

{% trans "Spatial Reference System" %}

-

{% blocktrans %}A spatial reference system (SRS) or coordinate reference system (CRS) is a coordinate-based local, - regional or global system used to locate geographical entities. A spatial reference system defines a specific map - projection, as well as transformations between different spatial reference systems. Spatial reference systems are - defined by the OGC's Simple feature access using well-known text, and support has been implemented by several - standards-based geographic information systems. Spatial reference systems can be referred to using a SRID integer, - including EPSG codes defined by the International Association of Oil and Gas Producers. - It is specified in ISO 19111:2007 Geographic information—Spatial referencing by coordinates, also published as - OGC Abstract Specification, Topic 2: Spatial referencing by coordinate.{% endblocktrans %}

-

{% trans "Identifiers" %}

-

- {% blocktrans %} - A Spatial Reference System Identifier (SRID) is a unique value used to unambiguously identify projected, unprojected, - and local spatial coordinate system definitions. These coordinate systems form the heart of all GIS applications. - - Virtually all major spatial vendors have created their own SRID implementation or refer to those of an authority, - such as the European Petroleum Survey Group (EPSG). - {% endblocktrans %} -

- -

{% blocktrans %}NOTE: As of 2005 the EPSG SRID values are now maintained by the International - Association of Oil & Gas Producers (OGP) Surveying & Positioning Committee{% endblocktrans %}

- -

- {% blocktrans %} - SRIDs are the primary key for the Open Geospatial Consortium (OGC) spatial_ref_sys metadata table for the Simple - Features for SQL Specification, Versions 1.1 and 1.2, which is defined as follows: - {% endblocktrans %} -

-
-    CREATE TABLE SPATIAL_REF_SYS
-    (
-        SRID      INTEGER   NOT NULL PRIMARY KEY,
-        AUTH_NAME CHARACTER VARYING(256),
-        AUTH_SRID INTEGER,
-        SRTEXT    CHARACTER VARYING(2048)
-    )
-                        
- -

- {% blocktrans %} - In spatially enabled databases (such as IBM DB2, IBM Informix, Microsoft SQL Server, MySQL, Oracle RDBMS, Teradata, PostGIS and - SQL Anywhere), SRIDs are used to uniquely identify the coordinate systems used to define columns of spatial data or individual - spatial objects in a spatial column (depending on the spatial implementation). SRIDs are typically associated with a well known - text (WKT) string definition of the coordinate system (SRTEXT, above). From the Well Known Text Wikipedia page - {% endblocktrans %} -

- -

{% blocktrans %}“A WKT string for a spatial reference system describes the datum, geoid, coordinate system, - and map projection of the spatial objects”.{% endblocktrans %}

- -

- {% blocktrans %} - Here are two common coordinate systems with their EPSG SRID value followed by their well known text: - {% endblocktrans %} -

- -

- {% blocktrans %} - UTM, Zone 17N, NAD27 — SRID 2029 - {% endblocktrans %} -

- -
-    PROJCS["NAD27(76) / UTM zone 17N",
-        GEOGCS["NAD27(76)",
-            DATUM["North_American_Datum_1927_1976",
-                SPHEROID["Clarke 1866",6378206.4,294.9786982138982,
-                    AUTHORITY["EPSG","7008"]],
-                AUTHORITY["EPSG","6608"]],
-            PRIMEM["Greenwich",0,
-                AUTHORITY["EPSG","8901"]],
-            UNIT["degree",0.01745329251994328,
-                AUTHORITY["EPSG","9122"]],
-            AUTHORITY["EPSG","4608"]],
-        UNIT["metre",1,
-            AUTHORITY["EPSG","9001"]],
-        PROJECTION["Transverse_Mercator"],
-        PARAMETER["latitude_of_origin",0],
-        PARAMETER["central_meridian",-81],
-        PARAMETER["scale_factor",0.9996],
-        PARAMETER["false_easting",500000],
-        PARAMETER["false_northing",0],
-        AUTHORITY["EPSG","2029"],
-        AXIS["Easting",EAST],
-        AXIS["Northing",NORTH]]
-                        
- -

- {% blocktrans %} - WGS84 — SRID 4326 - {% endblocktrans %} -

- -
-    GEOGCS["WGS 84",
-        DATUM["WGS_1984",
-            SPHEROID["WGS 84",6378137,298.257223563,
-                AUTHORITY["EPSG","7030"]],
-            AUTHORITY["EPSG","6326"]],
-        PRIMEM["Greenwich",0,
-            AUTHORITY["EPSG","8901"]],
-        UNIT["degree",0.01745329251994328,
-            AUTHORITY["EPSG","9122"]],
-        AUTHORITY["EPSG","4326"]]
-                        
- -

- {% blocktrans %} - SRID values associated with spatial data can be used to constrain spatial operations — for instance, spatial operations cannot be performed - between spatial objects with differing SRIDs in some systems, or trigger coordinate system transformations between spatial objects in others. - {% endblocktrans %} -

- -

{% trans "EPSG Code (Source SRS)" %}

-

{% blocktrans %}Source SRS EPSG Code is mandatory and represents the native data Spatial Reference System. This must be coherent with the - Geometry values (lon/lat coordinates as an instance) stored on the geospatial dataset. If not specified on the geospatial data itself, it - must be manually declared by the operator.{% endblocktrans %}

-

{% trans "EPSG Code (Target SRS)" %}

-

{% blocktrans %}Target SRS EPSG Code is optional. This must be used only if we need to re-project the coordinates from Source SRS to another one. - {% endblocktrans %} -

-
-
- -
-
-
-
-{% endblock %} - -{% block extra_script %} -{{ block.super }} - - -{% endblock %} diff --git a/geonode/upload/templates/upload/dataset_upload_csv.html b/geonode/upload/templates/upload/dataset_upload_csv.html deleted file mode 100644 index 3cf8ed7357e..00000000000 --- a/geonode/upload/templates/upload/dataset_upload_csv.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "upload/dataset_upload_base.html" %} -{% load i18n %} -{% load static %} -{% load client_lib_tags %} - -{% block body_class %}data data-list upload{% endblock %} - -{% block title %} {% trans "Upload Dataset Step: CSV Field Mapping" %} - {{ block.super }} {% endblock %} - -{% block head %} -{{ block.super }} -{% endblock %} - -{% block body %} -
-

{% trans "Geospatial Data" %} "{{ dataset_name }}"

- - {% if present_choices %} -

{% trans "Please indicate which attributes contain the latitude and longitude coordinates in the CSV data." %}

- {% if guessed_lat_or_lng %} -

{% blocktrans %}With this data, GeoNode was able to guess which attributes contain the - latitude and longitude coordinates, but please confirm that the correct - attributes are selected below.{% endblocktrans %}

- {% endif %} - -
- {% csrf_token %} - {% if error %} -
{{ error }}
- {% endif %} -
- - -
-
- - -
- - {% trans "Cancel" %}   - -
-
- -
-
-
-
- - {% else %} -

{% blocktrans %}We did not detect columns that could be used for the latitude and longitude. - Please verify that you have two columns in your csv file that can be used for - the latitude and longitude.{% endblocktrans %}

- {% endif %} -
-{% endblock %} - -{% block extra_script %} -{{ block.super }} - - -{% endblock %} diff --git a/geonode/upload/templates/upload/dataset_upload_error.html b/geonode/upload/templates/upload/dataset_upload_error.html deleted file mode 100644 index c9bbd22d057..00000000000 --- a/geonode/upload/templates/upload/dataset_upload_error.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "upload/dataset_upload_base.html" %} - -{% block body_class %}data data-list upload{% endblock %} - -{% block body %} -

{{ error_msg|safe }}

-{% endblock %} diff --git a/geonode/upload/templates/upload/dataset_upload_invalid.html b/geonode/upload/templates/upload/dataset_upload_invalid.html deleted file mode 100644 index b7567bc169e..00000000000 --- a/geonode/upload/templates/upload/dataset_upload_invalid.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "upload/dataset_upload_base.html" %} -{% load i18n %} -{% load client_lib_tags %} -{% block body_class %}data data-list upload{% endblock %} - -{% block body %} -

{% trans "Your upload is either complete or you haven't resumed an earlier one." %}

-

{% trans "Return to" %} {% trans "Upload Form" %}.

-{% endblock %} diff --git a/geonode/upload/templates/upload/dataset_upload_metadata_base.html b/geonode/upload/templates/upload/dataset_upload_metadata_base.html index f2a9ae5bcc7..0266672063e 100644 --- a/geonode/upload/templates/upload/dataset_upload_metadata_base.html +++ b/geonode/upload/templates/upload/dataset_upload_metadata_base.html @@ -16,4 +16,4 @@

{% trans "Upload Dataset Metadata" %}

{% block body %}{% endblock body %} {% block sidebar %}{% endblock sidebar %}
-{% endblock body_outer %} +{% endblock body_outer %} \ No newline at end of file diff --git a/geonode/upload/templates/upload/dataset_upload_time.html b/geonode/upload/templates/upload/dataset_upload_time.html deleted file mode 100644 index c950ebaf4a1..00000000000 --- a/geonode/upload/templates/upload/dataset_upload_time.html +++ /dev/null @@ -1,442 +0,0 @@ -{% extends "upload/dataset_upload_base.html" %} -{% load i18n %} -{% load static %} -{% load dataset_tags %} -{% load client_lib_tags %} - -{% block body_class %}data data-list upload{% endblock %} - -{% block title %} {% trans "Time series configuration" %} - {{ block.super }} {% endblock %} -{% block page_title %}{% trans "Time series configuration" %}{% endblock page_title %} - -{% block head %} -{{ block.super }} -{% endblock %} - -{% block body %} -
-
- {% trans "Date time fields were detected inside" %} "{{ dataset_name }}". {% trans "Do you want to configure it as a time series dataset?" %} - -

{% blocktrans %}Toggling this selector allows you to configure (or not) this data as a time series; in this case you will also have to select an attribute - to drive the time dimension. -

- If GeoNode is not able to parse any of the values for the selected attribute red markers will appear to highlight the problems. -

- More information is provided at the bottom of the page in the "Additional Help" sections. - {% endblocktrans %} -

-
-
-
- {% csrf_token %} - -
-
-
-

{% trans "Configure as Time-Series" %}

- -
-
-
- -
-
- {% trans "Cancel" %}   - -
-
- - - - - - -{% endblock %} - -{% block extra_script %} -{{ block.super }} - - - - - -{% endblock %} diff --git a/geonode/upload/templates/upload/no_upload.html b/geonode/upload/templates/upload/no_upload.html deleted file mode 100644 index 642eca6476f..00000000000 --- a/geonode/upload/templates/upload/no_upload.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "upload/base.html" %} -{% load i18n %} -{% load client_lib_tags %} - -{% block body %} -
-

{% trans "Your upload is either complete or you haven't resumed an earlier one." %}

-

{% blocktrans %}Returning to the upload starting page in 5seconds {% endblocktrans %}.

-

{% blocktrans %} Or just go {% endblocktrans %}{% blocktrans %}now{% endblocktrans %}.

-
- -{% endblock %} diff --git a/geonode/upload/templatetags/upload_tags.py b/geonode/upload/templatetags/upload_tags.py deleted file mode 100644 index d1cf26834c1..00000000000 --- a/geonode/upload/templatetags/upload_tags.py +++ /dev/null @@ -1,93 +0,0 @@ -######################################################################### -# -# 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 import template - -register = template.Library() - - -@register.simple_tag -def upload_js(): - return """ - - - - -""" diff --git a/geonode/upload/tests/__init__.py b/geonode/upload/tests/__init__.py index 385aeb6da1f..50b39831df7 100644 --- a/geonode/upload/tests/__init__.py +++ b/geonode/upload/tests/__init__.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2024 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 @@ -16,166 +16,3 @@ # along with this program. If not, see . # ######################################################################### - -from geonode.tests.base import GeoNodeBaseTestSupport - -import os -import zipfile -import contextlib - -import geonode.upload.files as files -from geonode.utils import mkdtemp -from geonode.upload.files import SpatialFiles, scan_file -from geonode.upload.files import _rename_files, _contains_bad_names - - -@contextlib.contextmanager -def create_files(names, zipped=False): - tmpdir = mkdtemp() - names = [os.path.join(tmpdir, f) for f in names] - for f in names: - # required for windows to read the shapefile in binary mode and the zip - # in non-binary - if zipped: - open(f, "w").close() - else: - try: - open(f, "wb").close() - except OSError: - # windows fails at writing special characters - # need to do something better here - print("Test does not work in Windows") - if zipped: - basefile = os.path.join(tmpdir, "files.zip") - zf = zipfile.ZipFile(basefile, "w", allowZip64=True) - with zf: - for f in names: - zf.write(f, os.path.basename(f)) - - for f in names: - os.unlink(f) - names = [basefile] - yield names - - -class FilesTests(GeoNodeBaseTestSupport): - def test_types(self): - for t in files.types: - self.assertTrue(t.code is not None) - self.assertTrue(t.name is not None) - self.assertTrue(t.dataset_type is not None) - - def test_contains_bad_names(self): - self.assertTrue(_contains_bad_names(["1", "a"])) - self.assertTrue(_contains_bad_names(["a", "foo-bar"])) - - def test_rename_files(self): - with create_files(["junk", "notjunky"]) as tests: - try: - renamed = files._rename_files(tests) - self.assertTrue(renamed[0].endswith("junk_y_")) - except OSError: - pass - - def test_rename_and_prepare(self): - with create_files(["109029_23.tiff", "notjunk"]) as tests: - tests = _rename_files(tests) - self.assertTrue(tests[0].endswith("_109029_23.tiff")) - self.assertTrue(tests[1].endswith("junk_y_")) - - def test_scan_file(self): - """ - Tests the scan_file function. - """ - exts = (".shp", ".shx", ".sld", ".xml", ".prj", ".dbf") - - with create_files([f"san_andres_y_providencia_location{s}" for s in exts]) as tests: - shp = [s for s in tests if s.endswith(".shp")][0] - spatial_files = scan_file(shp) - self.assertTrue(isinstance(spatial_files, SpatialFiles)) - - spatial_file = spatial_files[0] - self.assertEqual(shp, spatial_file.base_file) - self.assertTrue(spatial_file.file_type.matches("shp")) - self.assertEqual(len(spatial_file.auxillary_files), 3) - self.assertEqual(len(spatial_file.xml_files), 1) - self.assertTrue(all(s.endswith("xml") for s in spatial_file.xml_files)) - self.assertEqual(len(spatial_file.sld_files), 1) - self.assertTrue(all(s.endswith("sld") for s in spatial_file.sld_files)) - - # Test the scan_file function with a zipped spatial file that needs to - # be renamed. - file_names = [ - "109029_23.shp", - "109029_23.shx", - "109029_23.dbf", - "109029_23.prj", - "109029_23.xml", - "109029_23.sld", - ] - with create_files(file_names, zipped=True) as tests: - spatial_files = scan_file(tests[0]) - self.assertTrue(isinstance(spatial_files, SpatialFiles)) - - spatial_file = spatial_files[0] - self.assertTrue(spatial_file.file_type.matches("shp")) - self.assertEqual(len(spatial_file.auxillary_files), 3) - self.assertEqual(len(spatial_file.xml_files), 1) - self.assertEqual(len(spatial_file.sld_files), 1) - self.assertTrue(all(s.endswith("xml") for s in spatial_file.xml_files)) - - basedir = os.path.dirname(spatial_file.base_file) - for f in file_names: - path = os.path.join(basedir, f"_{f}") - self.assertTrue(os.path.exists(path)) - - # Test the scan_file function with a raster spatial file takes SLD also. - file_names = ["109029_24.tif", "109029_24.sld"] - with create_files(file_names) as tests: - spatial_files = scan_file(tests[0]) - self.assertTrue(isinstance(spatial_files, SpatialFiles)) - - spatial_file = spatial_files[0] - self.assertTrue(spatial_file.file_type.matches("tif")) - self.assertEqual(len(spatial_file.auxillary_files), 0) - self.assertEqual(len(spatial_file.xml_files), 0) - self.assertEqual(len(spatial_file.sld_files), 1) - - -class TimeFormFormTest(GeoNodeBaseTestSupport): - def _form(self, data): - # prevent circular deps error - not sure why this module was getting - # imported during normal runserver execution but it was... - from geonode.upload.forms import TimeForm - - return TimeForm( - data, - time_names=["start_date", "end_date"], - text_names=["start_text", "end_text"], - year_names=["start_year", "end_year"], - ) - - def assert_start_end(self, data, start, end=None): - form = self._form(data) - self.assertTrue(form.is_valid()) - if start: - self.assertEqual(start, form.cleaned_data["start_attribute"]) - if end: - self.assertEqual(end, form.cleaned_data["end_attribute"]) - - def test_invalid_form(self): - form = self._form(dict(time_attribute="start_date", text_attribute="start_text")) - self.assertTrue(not form.is_valid()) - - def test_start_end_attribute_and_type(self): - self.assert_start_end(dict(time_attribute="start_date"), ("start_date", "Date")) - self.assert_start_end( - dict(text_attribute="start_text", end_year_attribute="end_year"), - ("start_text", "Text"), - ("end_year", "Number"), - ) - self.assert_start_end( - dict(year_attribute="start_year", end_time_attribute="end_date"), - ("start_year", "Number"), - ("end_date", "Date"), - ) diff --git a/geonode/upload/tests/end2end/__init__.py b/geonode/upload/tests/end2end/__init__.py new file mode 100644 index 00000000000..50b39831df7 --- /dev/null +++ b/geonode/upload/tests/end2end/__init__.py @@ -0,0 +1,18 @@ +######################################################################### +# +# Copyright (C) 2024 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/geonode/upload/tests/end2end/integration.py b/geonode/upload/tests/end2end/integration.py new file mode 100644 index 00000000000..f1fb6276729 --- /dev/null +++ b/geonode/upload/tests/end2end/integration.py @@ -0,0 +1,733 @@ +######################################################################### +# +# 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 . +# +######################################################################### + +""" +See the README.rst in this directory for details on running these tests. +@todo allow using a database other than `development.db` - for some reason, a + test db is not created when running using normal settings +@todo when using database settings, a test database is used and this makes it + difficult for cleanup to track the layers created between runs +@todo only test_time seems to work correctly with database backend test settings +""" + +from unittest import mock +from geonode.tests.base import GeoNodeBaseTestSupport + +import os.path +from django.conf import settings +from django.db import connections +from django.contrib.auth import get_user_model + +from geonode.base.models import Link +from geonode.layers.models import Dataset +from geonode.upload.models import UploadSizeLimit +from geonode.catalogue import get_catalogue +from geonode.tests.utils import upload_step, Client +from geonode.geoserver.helpers import ogc_server_settings, cascading_delete +from geonode.geoserver.signals import gs_catalog + +from geoserver.catalog import Catalog +from gisdata import BAD_DATA +from gisdata import GOOD_DATA +from owslib.wms import WebMapService +from zipfile import ZipFile + +import re +import os +import csv +import glob +from urllib.parse import unquote, urlsplit +from urllib.error import HTTPError +import logging +import tempfile +import unittest +import dj_database_url + +GEONODE_USER = "admin" +GEONODE_PASSWD = "admin" +GEONODE_URL = settings.SITEURL.rstrip("/") +GEOSERVER_URL = ogc_server_settings.LOCATION +GEOSERVER_USER, GEOSERVER_PASSWD = ogc_server_settings.credentials + +DB_HOST = settings.DATABASES["default"]["HOST"] +DB_PORT = settings.DATABASES["default"]["PORT"] +DB_NAME = settings.DATABASES["default"]["NAME"] +DB_USER = settings.DATABASES["default"]["USER"] +DB_PASSWORD = settings.DATABASES["default"]["PASSWORD"] +DATASTORE_URL = f"postgis://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" +postgis_db = dj_database_url.parse(DATASTORE_URL, conn_max_age=0) + +logging.getLogger("south").setLevel(logging.WARNING) +logger = logging.getLogger("importer") + + +# create test user if needed, delete all layers and set password +u, created = get_user_model().objects.get_or_create(username=GEONODE_USER) +if created: + u.first_name = "Jhònà" + u.last_name = "çénü" + u.set_password(GEONODE_PASSWD) + u.save() +else: + Dataset.objects.filter(owner=u).delete() + + +def get_wms(version="1.1.1", type_name=None, username=None, password=None): + """Function to return an OWSLib WMS object""" + # right now owslib does not support auth for get caps + # requests. Either we should roll our own or fix owslib + if type_name: + url = f"{GEOSERVER_URL}{type_name.replace(':', '/')}wms?request=getcapabilities" + else: + url = f"{GEOSERVER_URL}wms?request=getcapabilities" + ogc_server_settings = settings.OGC_SERVER["default"] + if username and password: + return WebMapService( + url, version=version, username=username, password=password, timeout=ogc_server_settings.get("TIMEOUT", 60) + ) + else: + return WebMapService(url, timeout=ogc_server_settings.get("TIMEOUT", 60)) + + +class UploaderBase(GeoNodeBaseTestSupport): + type = "dataset" + + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + if os.path.exists("integration_settings.py"): + os.unlink("integration_settings.py") + + def setUp(self): + # await startup + cl = Client(GEONODE_URL, GEONODE_USER, GEONODE_PASSWD) + for i in range(10): + try: + cl.get_html("/", debug=False) + break + except Exception: + pass + + self.client = Client(GEONODE_URL, GEONODE_USER, GEONODE_PASSWD) + self.catalog = Catalog( + f"{GEOSERVER_URL}rest", + GEOSERVER_USER, + GEOSERVER_PASSWD, + retries=ogc_server_settings.MAX_RETRIES, + backoff_factor=ogc_server_settings.BACKOFF_FACTOR, + ) + + settings.DATABASES["default"]["NAME"] = DB_NAME + + connections["default"].settings_dict["ATOMIC_REQUESTS"] = False + connections["default"].connect() + + self._tempfiles = [] + + def _post_teardown(self): + pass + + def tearDown(self): + connections.databases["default"]["ATOMIC_REQUESTS"] = False + + for temp_file in self._tempfiles: + os.unlink(temp_file) + + # Cleanup + if settings.OGC_SERVER["default"].get("GEOFENCE_SECURITY_ENABLED", False): + from geonode.geoserver.security import delete_all_geofence_rules + + delete_all_geofence_rules() + + def check_dataset_geonode_page(self, path): + """Check that the final dataset page render's correctly after + an dataset is uploaded""" + # the final url for uploader process. This does a redirect to + # the final dataset page in geonode + resp, _ = self.client.get_html(path) + self.assertEqual(resp.status_code, 200) + self.assertTrue("content-type" in resp.headers) + + def check_dataset_geoserver_caps(self, type_name): + """Check that a dataset shows up in GeoServer's get + capabilities document""" + # using owslib + wms = get_wms(type_name=type_name, username=GEOSERVER_USER, password=GEOSERVER_PASSWD) + ws, dataset_name = type_name.split(":") + self.assertTrue(dataset_name in wms.contents, f"{dataset_name} is not in {wms.contents}") + + def check_dataset_geoserver_rest(self, dataset_name): + """Check that a dataset shows up in GeoServer rest api after + the uploader is done""" + # using gsconfig to test the geoserver rest api. + dataset = self.catalog.get_layer(dataset_name) + self.assertIsNotNone(dataset) + + def check_and_pass_through_timestep(self, redirect_to): + time_step = upload_step("time") + srs_step = upload_step("srs") + if srs_step in redirect_to: + resp = self.client.make_request(redirect_to) + else: + self.assertTrue(time_step in redirect_to) + resp = self.client.make_request(redirect_to) + token = self.client.get_csrf_token(True) + self.assertEqual(resp.status_code, 200) + resp = self.client.make_request(redirect_to, {"csrfmiddlewaretoken": token}, ajax=True) + return resp, resp.json() + + def complete_raster_upload(self, file_path, resp, data): + return self.complete_upload(file_path, resp, data, is_raster=True) + + def check_save_step(self, resp, data): + """Verify the initial save step""" + self.assertEqual(resp.status_code, 200) + self.assertTrue(isinstance(data, dict)) + # make that the upload returns a success True key + self.assertTrue(data["success"], f"expected success but got {data}") + self.assertTrue("redirect_to" in data) + + def complete_upload(self, file_path, resp, data, is_raster=False): + """Method to check if a dataset was correctly uploaded to the + GeoNode. + + arguments: file path, the django http response + + Checks to see if a dataset is configured in Django + Checks to see if a dataset is configured in GeoServer + checks the Rest API + checks the get cap document""" + + dataset_name, ext = os.path.splitext(os.path.basename(file_path)) + + if not isinstance(data, str): + self.check_save_step(resp, data) + + dataset_page = self.finish_upload(data["redirect_to"], dataset_name, is_raster) + + self.check_dataset_complete(dataset_page, dataset_name) + + def finish_upload(self, current_step, dataset_name, is_raster=False, skip_srs=False): + if not is_raster: + resp, data = self.check_and_pass_through_timestep(current_step) + self.assertEqual(resp.status_code, 200) + if not isinstance(data, str): + if data["success"]: + self.assertTrue(data["success"], f"expected success but got {data}") + self.assertTrue("redirect_to" in data) + current_step = data["redirect_to"] + # self.wait_for_progress(data.get('progress')) + + if not is_raster and not skip_srs: + self.assertTrue(upload_step("srs") in current_step) + # if all is good, the srs step will redirect to the final page + final_step = current_step.replace("srs", "final") + resp = self.client.make_request(final_step) + else: + self.assertTrue( + urlsplit(upload_step("final")).path in current_step, + f"current_step: {current_step} - upload_step('final'): {upload_step('final')}", + ) + resp = self.client.get(current_step) + + self.assertEqual(resp.status_code, 200) + try: + c = resp.json() + url = c["url"] + url = unquote(url) + # and the final page should redirect to the dataset page + # @todo - make the check match completely (endswith at least) + # currently working around potential 'orphaned' db tables + self.assertTrue(dataset_name in url, f"expected {dataset_name} in URL, got {url}") + return url + except Exception: + return current_step + + def check_dataset_complete(self, dataset_page, original_name): + """check everything to verify the dataset is complete""" + self.check_dataset_geonode_page(dataset_page) + # @todo use the original_name + # currently working around potential 'orphaned' db tables + # this grabs the name from the url (it might contain a 0) + type_name = os.path.basename(dataset_page) + dataset_name = original_name + try: + dataset_name = type_name.split(":")[1] + except Exception: + pass + + # work around acl caching on geoserver side of things + caps_found = False + for i in range(10): + try: + self.check_dataset_geoserver_caps(type_name) + self.check_dataset_geoserver_rest(dataset_name) + caps_found = True + except Exception: + pass + if not caps_found: + logger.warning(f"Could not recognize Dataset {original_name} on GeoServer WMS Capa") + + def check_invalid_projection(self, dataset_name, resp, data): + """Makes sure that we got the correct response from an dataset + that can't be uploaded""" + self.assertTrue(resp.status_code, 200) + if not isinstance(data, str): + self.assertTrue(data["success"]) + srs_step = upload_step("srs") + if "srs" in data["redirect_to"]: + self.assertTrue(srs_step in data["redirect_to"]) + resp, soup = self.client.get_html(data["redirect_to"]) + # grab an h2 and find the name there as part of a message saying it's + # bad + h2 = soup.find_all(["h2"])[0] + self.assertTrue(str(h2).find(dataset_name)) + + def check_upload_complete(self, dataset_name, resp, data): + """Makes sure that we got the correct response from an dataset + that has been uploaded""" + self.assertTrue(resp.status_code, 200) + if not isinstance(data, str): + self.assertTrue(data["success"]) + final_step = upload_step("final") + if "final" in data["redirect_to"]: + self.assertTrue(final_step in data["redirect_to"]) + + def check_upload_failed(self, dataset_name, resp, data): + """Makes sure that we got the correct response from an dataset + that can't be uploaded""" + self.assertTrue(resp.status_code, 400) + + def upload_folder_of_files(self, folder, final_check, session_ids=None): + mains = (".tif", ".shp", ".zip", ".asc") + + def is_main(_file): + _, ext = os.path.splitext(_file) + return ext.lower() in mains + + for main in filter(is_main, os.listdir(folder)): + # get the abs path to the file + _file = os.path.join(folder, main) + base, _ = os.path.splitext(_file) + resp, data = self.client.upload_file(_file) + if session_ids is not None: + if not isinstance(data, str) and data.get("url"): + session_id = re.search(r".*id=(\d+)", data.get("url")).group(1) + if session_id: + session_ids += [session_id] + if not isinstance(data, str): + self.wait_for_progress(data.get("progress")) + final_check(base, resp, data) + + def upload_file(self, fname, final_check, check_name=None, session_ids=None): + if not check_name: + check_name, _ = os.path.splitext(fname) + logger.error(f" debug CircleCI...........upload_file: {fname}") + resp, data = self.client.upload_file(fname) + if session_ids is not None: + if not isinstance(data, str): + if data.get("url"): + session_id = re.search(r".*id=(\d+)", data.get("url")).group(1) + if session_id: + session_ids += [session_id] + if not isinstance(data, str): + logger.error(f" debug CircleCI...........wait_for_progress: {data.get('progress')}") + self.wait_for_progress(data.get("progress")) + final_check(check_name, resp, data) + + def wait_for_progress(self, progress_url, wait_for_progress_cnt=0): + if progress_url: + resp = self.client.get(progress_url) + json_data = resp.json() + logger.error(f" [{wait_for_progress_cnt}] debug CircleCI...........json_data: {json_data}") + # "COMPLETE" state means done + if json_data and json_data.get("state", "") == "COMPLETE": + return json_data + elif json_data and json_data.get("state", "") == "RUNNING" and wait_for_progress_cnt < 30: + logger.error(f"[{wait_for_progress_cnt}] ... wait_for_progress @ {progress_url}") + json_data = self.wait_for_progress(progress_url, wait_for_progress_cnt=wait_for_progress_cnt + 1) + return json_data + + def temp_file(self, ext): + fd, abspath = tempfile.mkstemp(ext) + self._tempfiles.append(abspath) + return fd, abspath + + def make_csv(self, fieldnames, *rows): + fd, abspath = self.temp_file(".csv") + with open(abspath, "w", newline="") as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + for r in rows: + writer.writerow(r) + return abspath + + +class TestUpload(UploaderBase): + def test_shp_upload(self): + """Tests if a vector dataset can be uploaded to a running GeoNode/GeoServer""" + dataset_name = "san_andres_y_providencia_water" + fname = os.path.join(GOOD_DATA, "vector", f"{dataset_name}.shp") + self.upload_file(fname, self.complete_upload, check_name=f"{dataset_name}") + + test_dataset = Dataset.objects.filter(name__icontains=f"{dataset_name}").last() + if test_dataset: + dataset_attributes = test_dataset.attributes + self.assertIsNotNone(dataset_attributes) + + # Links + _def_link_types = ["original", "metadata"] + _links = Link.objects.filter(link_type__in=_def_link_types) + # Check 'original' and 'metadata' links exist + self.assertIsNotNone(_links, "No 'original' and 'metadata' links have been found") + self.assertTrue(_links.exists(), "No 'original' and 'metadata' links have been found") + # Check original links in csw_anytext + _post_migrate_links_orig = Link.objects.filter( + resource=test_dataset.resourcebase_ptr, + resource_id=test_dataset.resourcebase_ptr.id, + link_type="original", + ) + + for _link_orig in _post_migrate_links_orig: + if _link_orig.url not in test_dataset.csw_anytext: + logger.error(f"The link URL {_link_orig.url} not found in {test_dataset} 'csw_anytext' attribute") + # TODO: this check is randomly failing on CircleCI... we need to understand how to stabilize it + # self.assertIn( + # _link_orig.url, + # test_dataset.csw_anytext, + # f"The link URL {_link_orig.url} is not present in the 'csw_anytext' \ + # attribute of the dataset '{test_dataset.alternate}'" + # ) + # Check catalogue + catalogue = get_catalogue() + record = catalogue.get_record(test_dataset.uuid) + self.assertIsNotNone(record) + self.assertTrue( + hasattr(record, "links"), + f"No records have been found in the catalogue for the resource '{test_dataset.alternate}'", + ) + # Check 'metadata' links for each record + for mime, name, metadata_url in record.links["metadata"]: + try: + _post_migrate_link_meta = Link.objects.get( + resource=test_dataset.resourcebase_ptr, + url=metadata_url, + name=name, + extension="xml", + mime=mime, + link_type="metadata", + ) + self.assertIsNotNone( + _post_migrate_link_meta, + f"No '{name}' links have been found in the catalogue for the resource '{test_dataset.alternate}'", + ) + except Link.DoesNotExist: + _post_migrate_link_meta = None + + def test_raster_upload(self): + """Tests if a raster dataset can be upload to a running GeoNode GeoServer""" + fname = os.path.join(GOOD_DATA, "raster", "relief_san_andres.tif") + self.upload_file(fname, self.complete_raster_upload, check_name="relief_san_andres") + + test_dataset = Dataset.objects.all().first() + self.assertIsNotNone(test_dataset) + + def test_zipped_upload(self): + """Test uploading a zipped shapefile""" + fd, abspath = self.temp_file(".zip") + fp = os.fdopen(fd, "wb") + zf = ZipFile(fp, "w", allowZip64=True) + with zf: + fpath = os.path.join(GOOD_DATA, "vector", "san_andres_y_providencia_poi.*") + for f in glob.glob(fpath): + zf.write(f, os.path.basename(f)) + + self.upload_file(abspath, self.complete_upload, check_name="san_andres_y_providencia_poi") + layer = Dataset.objects.filter(name__contains="san_andres_y_providencia_poi").first() + self.assertIsNotNone(layer.default_style) + try: + from geonode.geoserver.helpers import gs_catalog + + gs_layer = gs_catalog.get_layer(layer.name) + if gs_layer: + self.assertIsNotNone(gs_layer.default_style) + except Exception as e: + logger.exception(e) + + def test_geonode_same_UUID_error(self): + """ + Ensure a new dataset with same UUID metadata cannot be uploaded + """ + PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) + + # Uploading the first one should be OK + same_uuid_a = os.path.join(PROJECT_ROOT, "data/same_uuid_a.zip") + self.upload_file(same_uuid_a, self.complete_upload, check_name="same_uuid_a") + + # Uploading the second one should give an ERROR + same_uuid_b = os.path.join(PROJECT_ROOT, "data/same_uuid_b.zip") + self.upload_file(same_uuid_b, self.check_upload_failed) + + def test_ascii_grid_upload(self): + """Tests the layers that ASCII grid files are uploaded along with aux""" + session_ids = [] + + PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) + thedataset_path = os.path.join(PROJECT_ROOT, "data/arc_sample") + self.upload_folder_of_files(thedataset_path, self.complete_raster_upload, session_ids=session_ids) + + def test_invalid_dataset_upload(self): + """Tests the layers that are invalid and should not be uploaded""" + # this issue with this test is that the importer supports + # shapefiles without an .prj + session_ids = [] + + invalid_path = os.path.join(BAD_DATA) + self.upload_folder_of_files(invalid_path, self.check_invalid_projection, session_ids=session_ids) + + def test_coherent_importer_session(self): + """Tests that the upload computes correctly next session IDs""" + session_ids = [] + + # First of all lets upload a raster + fname = os.path.join(GOOD_DATA, "raster", "relief_san_andres.tif") + logger.error(f" debug CircleCI...........fname: {fname}") + self.assertTrue(os.path.isfile(fname)) + self.upload_file(fname, self.complete_raster_upload, session_ids=session_ids) + + # Next force an invalid session + invalid_path = os.path.join(BAD_DATA) + logger.error(f" debug CircleCI...........invalid_path: {invalid_path}") + self.upload_folder_of_files(invalid_path, self.check_invalid_projection, session_ids=session_ids) + + # Finally try to upload a good file and check the session IDs + fname = os.path.join(GOOD_DATA, "raster", "relief_san_andres.tif") + logger.error(f" debug CircleCI...........fname: {fname}") + self.upload_file(fname, self.complete_raster_upload, session_ids=session_ids) + + self.assertTrue(len(session_ids) >= 0) + if len(session_ids) > 1: + self.assertTrue(int(session_ids[0]) < int(session_ids[1])) + + def test_extension_not_implemented(self): + """Verify a error message is return when an unsupported dataset is + uploaded""" + + # try to upload ourselves + # a python file is unsupported + unsupported_path = __file__ + if unsupported_path.endswith(".pyc"): + unsupported_path = unsupported_path.rstrip("c") + + with self.assertRaises(HTTPError): + self.client.upload_file(unsupported_path) + + def test_csv(self): + """make sure a csv upload fails gracefully/normally when not activated""" + csv_file = self.make_csv(["lat", "lon", "thing"], {"lat": -100, "lon": -40, "thing": "foo"}) + dataset_name, ext = os.path.splitext(os.path.basename(csv_file)) + resp, data = self.client.upload_file(csv_file) + self.assertEqual(resp.status_code, 200) + if not isinstance(data, str): + self.assertTrue("success" in data) + self.assertTrue(data["success"]) + self.assertTrue(data["redirect_to"], "/upload/csv") + + def test_csv_with_size_limit(self): + """make sure a upload fails gracefully/normally with big files""" + upload_size_limit_obj, created = UploadSizeLimit.objects.get_or_create( + slug="dataset_upload_size", + defaults={ + "description": "The sum of sizes for the files of a dataset upload.", + "max_size": 1, + }, + ) + upload_size_limit_obj.max_size = 1 + upload_size_limit_obj.save() + + handler_upload_size_limit_obj, created = UploadSizeLimit.objects.get_or_create( + slug="file_upload_handler", + defaults={ + "description": ( + "Request total size, validated before the upload process. " + 'This should be greater than "dataset_upload_size".' + ), + "max_size": 1024, + }, + ) + handler_upload_size_limit_obj.max_size = 1024 # Greater than 689 bytes (test csv request size) + handler_upload_size_limit_obj.save() + + csv_file = self.make_csv(["lat", "lon", "thing"], {"lat": -100, "lon": -40, "thing": "foo"}) + with self.assertRaises(HTTPError) as error: + self.client.upload_file(csv_file) + expected_error = "Total upload size exceeds 1\\u00a0byte. " "Please try again with smaller files." + self.assertIn(expected_error, error.exception.msg) + + def test_csv_with_upload_handler_size_limit(self): + """make sure a upload fails gracefully/normally with big files""" + # Set ``dataset_upload_size`` to 3 and to ``file_upload_handler`` 2 + # In production ``dataset_upload_size`` should not be greater than ``file_upload_handler`` + # It's used here to make sure that the uploadhandler is called + self.client.login() + expected_error = "Total upload size exceeds 1\xa0byte. Please try again with smaller files." + + total_upload_size_limit_obj, created = UploadSizeLimit.objects.get_or_create( + slug="dataset_upload_size", + defaults={ + "description": "The sum of sizes for the files of a dataset upload.", + "max_size": 1024, + }, + ) + total_upload_size_limit_obj.max_size = 1024 # Greater than 689 bytes (test csv request size) + total_upload_size_limit_obj.save() + + csv_file = self.make_csv(["lat", "lon", "thing"], {"lat": -100, "lon": -40, "thing": "foo"}) + + max_size_path = "geonode.upload.uploadhandler.SizeRestrictedFileUploadHandler._get_max_size" + + with mock.patch(max_size_path, new_callable=mock.PropertyMock) as max_size_mock: + max_size_mock.return_value = lambda x: 2 + with self.assertRaises(HTTPError) as error: + self.client.upload_file(csv_file) + expected_error = "Unexpected exception Expecting value: line 1 column 1 (char 0)" + self.assertIn(expected_error, error.exception.msg) + + +@unittest.skipUnless(ogc_server_settings.datastore_db, "Vector datastore not enabled") +class TestUploadDBDataStore(UploaderBase): + def test_csv(self): + """Override the baseclass test and verify a correct CSV upload""" + + csv_file = self.make_csv(["lat", "lon", "thing"], {"lat": -100, "lon": -40, "thing": "foo"}) + dataset_name, ext = os.path.splitext(os.path.basename(csv_file)) + resp, form_data = self.client.upload_file(csv_file) + self.assertEqual(resp.status_code, 200) + if not isinstance(form_data, str): + self.check_save_step(resp, form_data) + csv_step = form_data["redirect_to"] + self.assertTrue(upload_step("csv") in csv_step) + form_data = dict(lat="lat", lng="lon", csrfmiddlewaretoken=self.client.get_csrf_token()) + resp = self.client.make_request(csv_step, form_data) + content = resp.json() + self.assertEqual(resp.status_code, 200) + self.assertEqual(content["status"], "incomplete") + + def test_time(self): + """Verify that uploading time based shapefile works properly""" + cascading_delete(dataset_name="boxes_with_date", catalog=self.catalog) + + timedir = os.path.join(GOOD_DATA, "time") + dataset_name = "boxes_with_date" + shp = os.path.join(timedir, f"{dataset_name}.shp") + + # get to time step + resp, data = self.client.upload_file(shp) + self.assertEqual(resp.status_code, 200) + if not isinstance(data, str): + # self.wait_for_progress(data.get('progress')) + self.assertTrue(data["success"]) + self.assertTrue(data["redirect_to"], upload_step("time")) + redirect_to = data["redirect_to"] + resp, data = self.client.get_html(upload_step("time")) + self.assertEqual(resp.status_code, 200) + data = dict( + csrfmiddlewaretoken=self.client.get_csrf_token(), + time_attribute="date", + presentation_strategy="LIST", + ) + resp = self.client.make_request(redirect_to, data) + self.assertEqual(resp.status_code, 200) + resp_js = resp.json() + if resp_js["success"]: + url = resp_js["redirect_to"] + + resp = self.client.make_request(url, data) + + url = resp.json()["url"] + + self.assertTrue(url.endswith(dataset_name), f"expected url to end with {dataset_name}, but got {url}") + self.assertEqual(resp.status_code, 200) + + url = unquote(url) + self.check_dataset_complete(url, dataset_name) + wms = get_wms(type_name=f"geonode:{dataset_name}", username=GEOSERVER_USER, password=GEOSERVER_PASSWD) + dataset_info = list(wms.items())[0][1] + self.assertEqual(100, len(dataset_info.timepositions)) + else: + self.assertTrue("error_msg" in resp_js) + + def test_configure_time(self): + dataset_name = "boxes_with_end_date" + # make sure it's not there (and configured) + cascading_delete(dataset_name=dataset_name, catalog=gs_catalog) + + def get_wms_timepositions(): + alternate_name = f"geonode:{dataset_name}" + if alternate_name in get_wms().contents: + metadata = get_wms().contents[alternate_name] + self.assertTrue(metadata is not None) + return metadata.timepositions + else: + return None + + thefile = os.path.join(GOOD_DATA, "time", f"{dataset_name}.shp") + # Test upload with custom permissions + resp, data = self.client.upload_file(thefile, perms='{"users": {"AnonymousUser": []}, "groups":{}}') + _dataset = Dataset.objects.get(name=dataset_name) + _user = get_user_model().objects.get(username="AnonymousUser") + self.assertEqual(_dataset.get_user_perms(_user).count(), 0) + + # initial state is no positions or info + self.assertTrue(get_wms_timepositions() is None) + self.assertEqual(resp.status_code, 200) + + # enable using interval and single attribute + if not isinstance(data, str): + # self.wait_for_progress(data.get('progress')) + self.assertTrue(data["success"]) + self.assertTrue(data["redirect_to"], upload_step("time")) + redirect_to = data["redirect_to"] + resp, data = self.client.get_html(upload_step("time")) + self.assertEqual(resp.status_code, 200) + data = dict( + csrfmiddlewaretoken=self.client.get_csrf_token(), + time_attribute="date", + time_end_attribute="enddate", + presentation_strategy="LIST", + ) + resp = self.client.make_request(redirect_to, data) + self.assertEqual(resp.status_code, 200) + resp_js = resp.json() + if resp_js["success"]: + url = resp_js["redirect_to"] + resp = self.client.make_request(url, data) + url = resp.json()["url"] + self.assertTrue(url.endswith(dataset_name), f"expected url to end with {dataset_name}, but got {url}") + self.assertEqual(resp.status_code, 200) + url = unquote(url) + self.check_dataset_complete(url, dataset_name) + wms = get_wms(type_name=f"geonode:{dataset_name}", username=GEOSERVER_USER, password=GEOSERVER_PASSWD) + dataset_info = list(wms.items())[0][1] + self.assertEqual(100, len(dataset_info.timepositions)) + else: + self.assertTrue("error_msg" in resp_js) diff --git a/geonode/upload/tests/end2end/test_end2end.py b/geonode/upload/tests/end2end/test_end2end.py new file mode 100644 index 00000000000..3d93c65f075 --- /dev/null +++ b/geonode/upload/tests/end2end/test_end2end.py @@ -0,0 +1,497 @@ +######################################################################### +# +# Copyright (C) 2024 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 ast +import os +import time + +import mock +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import override_settings +from django.urls import reverse +from dynamic_models.models import FieldSchema, ModelSchema +from geonode.layers.models import Dataset +from geonode.resource.models import ExecutionRequest +from geonode.utils import OGC_Servers_Handler +from geoserver.catalog import Catalog +from geonode.upload import project_dir +from geonode.upload.tests.utils import ImporterBaseTestSupport +import gisdata +from django.db.models import Q +from geonode.base.models import ResourceBase +import logging +from geonode.harvesting.harvesters.wms import WebMapService +from django.forms.models import model_to_dict +from unittest import skip + +logger = logging.getLogger() +geourl = settings.GEODATABASE_URL + + +@override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o777, FILE_UPLOAD_PERMISSIONS=0o7777) +class BaseImporterEndToEndTest(ImporterBaseTestSupport): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.user = get_user_model().objects.exclude(username="Anonymous").first() + cls.valid_gkpg = f"{project_dir}/tests/fixture/valid.gpkg" + cls.valid_geojson = f"{project_dir}/tests/fixture/valid.geojson" + cls.no_crs_gpkg = f"{project_dir}/tests/fixture/noCrsTable.gpkg" + file_path = gisdata.PROJECT_ROOT + filename = os.path.join(file_path, "both/good/sangis.org/Airport/Air_Runways.shp") + cls.valid_shp = { + "base_file": filename, + "dbf_file": f"{file_path}/both/good/sangis.org/Airport/Air_Runways.dbf", + "prj_file": f"{file_path}/both/good/sangis.org/Airport/Air_Runways.prj", + "shx_file": f"{file_path}/both/good/sangis.org/Airport/Air_Runways.shx", + } + cls.valid_kml = f"{project_dir}/tests/fixture/valid.kml" + cls.valid_tif = f"{project_dir}/tests/fixture/test_raster.tif" + cls.valid_csv = f"{project_dir}/tests/fixture/valid.csv" + + cls.url = reverse("importer_upload") + ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"] + + _user, _password = ogc_server_settings.credentials + + cls.cat = Catalog(service_url=ogc_server_settings.rest, username=_user, password=_password) + + def setUp(self) -> None: + self.admin, _ = get_user_model().objects.get_or_create(username="admin") + self.admin.is_superuser = True + self.admin.is_staff = True + self.admin.save() + for el in Dataset.objects.all(): + el.delete() + + def tearDown(self) -> None: + super().tearDown() + for el in Dataset.objects.all(): + el.delete() + + def _cleanup_layers(self, name): + layer = [x for x in self.cat.get_resources() if name in x.name] + if layer: + for el in layer: + try: + self.cat.delete(el) + except: # noqa + pass + try: + ResourceBase.objects.filter(alternate__icontains=name).delete() + except: # noqa + pass + + def _assertimport( + self, + payload, + initial_name, + overwrite=False, + last_update=None, + skip_geoserver=False, + assert_payload=None, + keep_resource=False, + ): + try: + self.client.force_login(self.admin) + + response = self.client.post(self.url, data=payload) + self.assertEqual(201, response.status_code, response.json()) + + # if is async, we must wait. It will wait for 1 min before raise exception + if ast.literal_eval(os.getenv("ASYNC_SIGNALS", "False")): + tentative = 1 + while ( + ExecutionRequest.objects.get(exec_id=response.json().get("execution_id")) + != ExecutionRequest.STATUS_FINISHED + and tentative <= 10 + ): + time.sleep(10) + tentative += 1 + exc_obj = ExecutionRequest.objects.get(exec_id=response.json().get("execution_id")) + if exc_obj.status != ExecutionRequest.STATUS_FINISHED: + raise Exception(f"Async still in progress after 1 min of waiting: {model_to_dict(exc_obj)}") + + # check if the dynamic model is created + if os.getenv("IMPORTER_ENABLE_DYN_MODELS", False): + _schema_id = ModelSchema.objects.filter(name__icontains=initial_name) + self.assertTrue(_schema_id.exists()) + schema_entity = _schema_id.first() + self.assertTrue(FieldSchema.objects.filter(model_schema=schema_entity).exists()) + + # Verify that ogr2ogr created the table with some data in it + entries = ModelSchema.objects.filter(id=schema_entity.id).first() + self.assertTrue(entries.as_model().objects.exists()) + + # check if the geonode resource exists + resource = ResourceBase.objects.filter( + Q(alternate__icontains=f"geonode:{initial_name}") | Q(alternate__icontains=initial_name) + ) + self.assertTrue(resource.exists()) + + if not skip_geoserver: + resources = self.cat.get_resources() + # check if the resource is in geoserver + self.assertTrue(resource.first().title in res for res in [y.name for y in resources]) + if overwrite: + self.assertTrue(resource.first().last_updated > last_update) + + if assert_payload: + target = resource.first() + target.refresh_from_db() + for key, value in assert_payload.items(): + if hasattr(target, key): + self.assertEqual(getattr(target, key), value) + else: + logger.error(f"The attribute {key} doesn't belong to the resource.") + if keep_resource: + return resource.first() + finally: + if not keep_resource: + resource = ResourceBase.objects.filter( + Q(alternate__icontains=f"geonode:{initial_name}") | Q(alternate__icontains=initial_name) + ) + resource.delete() + + +class ImporterGeoPackageImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_geopackage(self): + self._cleanup_layers(name="stazioni_metropolitana") + + payload = { + "base_file": open(self.valid_gkpg, "rb"), + } + initial_name = "stazioni_metropolitana" + self._assertimport(payload, initial_name) + self._cleanup_layers(name="stazioni_metropolitana") + + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_gpkg_overwrite(self): + self._cleanup_layers(name="stazioni_metropolitana") + initial_name = "stazioni_metropolitana" + payload = { + "base_file": open(self.valid_gkpg, "rb"), + } + prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) + + payload = { + "base_file": open(self.valid_gkpg, "rb"), + } + payload["overwrite_existing_layer"] = True + payload["resource_pk"] = prev_dataset.pk + self._assertimport(payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated) + self._cleanup_layers(name="stazioni_metropolitana") + + +class ImporterNoCRSImportTest(BaseImporterEndToEndTest): + @override_settings(ASYNC_SIGNALS=False) + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + @skip("unknown fail on circleci") + def test_import_geopackage_with_no_crs_table(self): + + self._cleanup_layers(name="mattia_test") + payload = { + "base_file": open(self.no_crs_gpkg, "rb"), + } + initial_name = "mattia_test" + with self.assertLogs(level="ERROR") as _log: + self._assertimport(payload, initial_name) + + self.assertIn( + "The following layer layer_styles does not have a Coordinate Reference System (CRS) and will be skipped.", + [x.message for x in _log.records], + ) + + self._cleanup_layers(name="mattia_test") + + @override_settings(ASYNC_SIGNALS=False) + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + @mock.patch("geonode.upload.handlers.common.vector.BaseVectorFileHandler._select_valid_layers") + @override_settings(MEDIA_ROOT="/tmp/", ASSETS_ROOT="/tmp/") + def test_import_geopackage_with_no_crs_table_should_raise_error_if_all_layer_are_invalid( + self, _select_valid_layers + ): + _select_valid_layers.return_value = [] + + self._cleanup_layers(name="mattia_test") + payload = { + "base_file": open(self.no_crs_gpkg, "rb"), + "store_spatial_file": True, + } + + with self.assertLogs(level="ERROR") as _log: + self.client.force_login(self.admin) + + response = self.client.post(self.url, data=payload) + self.assertEqual(500, response.status_code) + + self.assertIn( + "No valid layers found", + [x.message for x in _log.records], + ) + + self._cleanup_layers(name="mattia_test") + + +class ImporterGeoJsonImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_geojson(self): + + self._cleanup_layers(name="valid") + + payload = { + "base_file": open(self.valid_geojson, "rb"), + } + initial_name = "valid" + self._assertimport(payload, initial_name) + + self._cleanup_layers(name="valid") + + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_geojson_overwrite(self): + self._cleanup_layers(name="valid") + payload = { + "base_file": open(self.valid_geojson, "rb"), + } + initial_name = "valid" + prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) + payload = { + "base_file": open(self.valid_geojson, "rb"), + } + payload["overwrite_existing_layer"] = True + payload["resource_pk"] = prev_dataset.pk + self._assertimport(payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated) + + self._cleanup_layers(name="valid") + + +class ImporterGCSVImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_geojson(self): + self._cleanup_layers(name="valid") + + payload = { + "base_file": open(self.valid_csv, "rb"), + } + initial_name = "valid" + self._assertimport(payload, initial_name) + self._cleanup_layers(name="valid") + + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_csv_overwrite(self): + self._cleanup_layers(name="valid") + payload = { + "base_file": open(self.valid_csv, "rb"), + } + initial_name = "valid" + prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) + + payload = { + "base_file": open(self.valid_csv, "rb"), + } + initial_name = "valid" + payload["overwrite_existing_layer"] = True + payload["resource_pk"] = prev_dataset.pk + self._assertimport(payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated) + self._cleanup_layers(name="valid") + + +class ImporterKMLImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_kml(self): + self._cleanup_layers(name="sample_point_dataset") + payload = { + "base_file": open(self.valid_kml, "rb"), + } + initial_name = "sample_point_dataset" + self._assertimport(payload, initial_name) + self._cleanup_layers(name="sample_point_dataset") + + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_kml_overwrite(self): + initial_name = "sample_point_dataset" + + self._cleanup_layers(name="sample_point_dataset") + payload = { + "base_file": open(self.valid_kml, "rb"), + } + prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) + + payload = { + "base_file": open(self.valid_kml, "rb"), + } + payload["overwrite_existing_layer"] = True + payload["resource_pk"] = prev_dataset.pk + self._assertimport(payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated) + self._cleanup_layers(name="sample_point_dataset") + + +class ImporterShapefileImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_shapefile(self): + self._cleanup_layers(name="air_Runways") + payload = {_filename: open(_file, "rb") for _filename, _file in self.valid_shp.items()} + initial_name = "air_Runways" + self._assertimport(payload, initial_name) + self._cleanup_layers(name="air_Runways") + + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_shapefile_overwrite(self): + + self._cleanup_layers(name="air_Runways") + payload = {_filename: open(_file, "rb") for _filename, _file in self.valid_shp.items()} + initial_name = "air_Runways" + prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) + payload = {_filename: open(_file, "rb") for _filename, _file in self.valid_shp.items()} + payload["overwrite_existing_layer"] = True + payload["resource_pk"] = prev_dataset.pk + self._assertimport( + payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated, keep_resource=True + ) + self._cleanup_layers(name="air_Runways") + + +class ImporterRasterImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_raster(self): + self._cleanup_layers(name="test_raster") + + payload = { + "base_file": open(self.valid_tif, "rb"), + } + initial_name = "test_raster" + self._assertimport(payload, initial_name) + self._cleanup_layers(name="test_raster") + + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_import_raster_overwrite(self): + initial_name = "test_raster" + + self._cleanup_layers(name="test_raster") + payload = { + "base_file": open(self.valid_tif, "rb"), + } + prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) + + payload = { + "base_file": open(self.valid_tif, "rb"), + } + initial_name = "test_raster" + payload["overwrite_existing_layer"] = True + payload["resource_pk"] = prev_dataset.pk + self._assertimport(payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated) + self._cleanup_layers(name="test_raster") + + +class Importer3dTilesImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data", "ASYNC_SIGNALS": "False"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data", ASYNC_SIGNALS=False) + def test_import_3dtiles(self): + payload = { + "url": "https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples/main/1.1/TilesetWithFullMetadata/tileset.json", + "title": "Remote Title", + "type": "3dtiles", + } + initial_name = "remote_title" + assert_payload = { + "subtype": "3dtiles", + "title": "Remote Title", + "resource_type": "dataset", + } + self._assertimport(payload, initial_name, skip_geoserver=True, assert_payload=assert_payload) + + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data", "ASYNC_SIGNALS": "False"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data", ASYNC_SIGNALS=False) + def test_import_3dtiles_overwrite(self): + payload = { + "url": "https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples/main/1.1/TilesetWithFullMetadata/tileset.json", + "title": "Remote Title", + "type": "3dtiles", + } + initial_name = "remote_title" + assert_payload = { + "subtype": "3dtiles", + "title": "Remote Title", + "resource_type": "dataset", + } + # Lets import the resource first but without deleting it + resource = self._assertimport( + payload, + initial_name, + skip_geoserver=True, + keep_resource=True, + assert_payload=assert_payload, + ) + prev_timestamp = resource.last_updated + # let's re-import it again with the overwrite mode activated + assert_payload = { + "subtype": "3dtiles", + "resource_type": "dataset", + } + payload["overwrite_existing_layer"] = True + self._assertimport( + payload, + initial_name, + skip_geoserver=True, + overwrite=True, + assert_payload=assert_payload, + last_update=prev_timestamp, + ) + + +class ImporterWMSImportTest(BaseImporterEndToEndTest): + @mock.patch.dict(os.environ, {"GEONODE_GEODATABASE": "test_geonode_data", "ASYNC_SIGNALS": "False"}) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data", ASYNC_SIGNALS=False) + def test_import_wms(self): + _, wms = WebMapService( + "https://development.demo.geonode.org/geoserver/ows?service=WMS&version=1.3.0&request=GetCapabilities" + ) + resource_to_take = next(iter(wms.contents)) + res = wms[next(iter(wms.contents))] + payload = { + "url": "https://development.demo.geonode.org/geoserver/ows?service=WMS&version=1.3.0&request=GetCapabilities", + "title": "Remote Title", + "type": "wms", + "lookup": resource_to_take, + "parse_remote_metadata": True, + } + initial_name = res.title + assert_payload = { + "subtype": "remote", + "title": res.title, + "resource_type": "dataset", + "sourcetype": "REMOTE", + "ptype": "gxp_wmscsource", + } + self._assertimport(payload, initial_name, skip_geoserver=True, assert_payload=assert_payload) diff --git a/geonode/upload/tests/end2end/test_end2end_copy.py b/geonode/upload/tests/end2end/test_end2end_copy.py new file mode 100644 index 00000000000..630e16962b4 --- /dev/null +++ b/geonode/upload/tests/end2end/test_end2end_copy.py @@ -0,0 +1,216 @@ +######################################################################### +# +# Copyright (C) 2024 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 ast +import os +import time +from django.http import QueryDict +import gisdata +import mock +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import override_settings +from django.urls import reverse +from dynamic_models.models import FieldSchema, ModelSchema +from geonode.layers.models import Dataset +from geonode.resource.models import ExecutionRequest +from geonode.utils import OGC_Servers_Handler +from geoserver.catalog import Catalog +from geonode.upload import project_dir +from geonode.upload.tests.utils import TransactionImporterBaseTestSupport +from django.db import transaction +from django.forms.models import model_to_dict + +geourl = settings.GEODATABASE_URL + + +class BaseClassEnd2End(TransactionImporterBaseTestSupport): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.valid_gkpg = f"{project_dir}/tests/fixture/valid.gpkg" + cls.valid_geojson = f"{project_dir}/tests/fixture/valid.geojson" + file_path = gisdata.PROJECT_ROOT + filename = os.path.join(file_path, "both/good/sangis.org/Airport/Air_Runways.shp") + cls.valid_shp = { + "base_file": filename, + "dbf_file": f"{file_path}/both/good/sangis.org/Airport/Air_Runways.dbf", + "prj_file": f"{file_path}/both/good/sangis.org/Airport/Air_Runways.prj", + "shx_file": f"{file_path}/both/good/sangis.org/Airport/Air_Runways.shx", + } + cls.url_create = reverse("importer_upload") + ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"] + cls.valid_kml = f"{project_dir}/tests/fixture/valid.kml" + cls.url_create = reverse("importer_upload") + ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"] + + _user, _password = ogc_server_settings.credentials + + cls.cat = Catalog(service_url=ogc_server_settings.rest, username=_user, password=_password) + + def setUp(self) -> None: + for el in Dataset.objects.all(): + el.delete() + + self.admin = get_user_model().objects.get(username="admin") + + def tearDown(self) -> None: + for el in Dataset.objects.all(): + el.delete() + + def _assertCloning(self, initial_name): + # getting the geonode resource + print(initial_name) + dataset = Dataset.objects.get(alternate__icontains=f"geonode:{initial_name}") + prev_dataset_count = Dataset.objects.count() + self.client.force_login(get_user_model().objects.get(username="admin")) + # creating the url and login + _url = reverse("importer_resource_copy", args=[dataset.id]) + + # defining the payload + payload = QueryDict("", mutable=True) + payload.update({"defaults": '{"title":"title_of_the_cloned_resource"}'}) + + # calling the endpoint + response = self.client.put(_url, data=payload, content_type="application/json") + self.assertEqual(200, response.status_code) + self._wait_execution(response) + + # checking if a new resource is created + self.assertEqual(prev_dataset_count + 1, Dataset.objects.count()) + + # check if the geonode resource exists + dataset = Dataset.objects.filter(title="title_of_the_cloned_resource") + self.assertTrue(dataset.exists()) + dataset = dataset.first() + # check if the dynamic model is created + _schema_id = ModelSchema.objects.filter(name__icontains=dataset.alternate.split(":")[1]) + self.assertTrue(_schema_id.exists()) + schema_entity = _schema_id.first() + self.assertTrue(FieldSchema.objects.filter(model_schema=schema_entity).exists()) + + # Verify that ogr2ogr created the table with some data in it + entries = ModelSchema.objects.filter(id=schema_entity.id).first() + self.assertTrue(entries.as_model().objects.exists()) + + # check if the resource is in geoserver + resources = self.cat.get_resources() + self.assertTrue(schema_entity.name in [y.name for y in resources]) + + def _import_resource(self, payload, initial_name): + _url = reverse("importer_upload") + self.client.force_login(get_user_model().objects.get(username="admin")) + + response = self.client.post(_url, data=payload) + self.assertEqual(201, response.status_code) + self._wait_execution(response) + + def _wait_execution(self, response, _id="execution_id"): + # if is async, we must wait. It will wait for 1 min before raise exception + if ast.literal_eval(os.getenv("ASYNC_SIGNALS", "False")): + print("is false") + tentative = 1 + while ( + ExecutionRequest.objects.get(exec_id=response.json().get(_id)) != ExecutionRequest.STATUS_FINISHED + and tentative <= 6 + ): + time.sleep(10) + tentative += 1 + exc_obj = ExecutionRequest.objects.get(exec_id=response.json().get(_id)) + if exc_obj.status != ExecutionRequest.STATUS_FINISHED: + print(ExecutionRequest.objects.get(exec_id=response.json().get(_id))) + raise Exception(f"Async still in progress after 1 min of waiting: {model_to_dict(exc_obj)}") + + +class ImporterCopyEnd2EndGpkgTest(BaseClassEnd2End): + @mock.patch.dict( + os.environ, + { + "GEONODE_GEODATABASE": "test_geonode_data", + "IMPORTER_ENABLE_DYN_MODELS": "True", + }, + ) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_copy_dataset_from_geopackage(self): + payload = { + "base_file": open(self.valid_gkpg, "rb"), + } + initial_name = "stazioni_metropolitana" + # first we need to import a resource + with transaction.atomic(): + self._import_resource(payload, initial_name) + + self._assertCloning(initial_name) + + +class ImporterCopyEnd2EndGeoJsonTest(BaseClassEnd2End): + @mock.patch.dict( + os.environ, + { + "GEONODE_GEODATABASE": "test_geonode_data", + "IMPORTER_ENABLE_DYN_MODELS": "True", + }, + ) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_copy_dataset_from_geojson(self): + payload = { + "base_file": open(self.valid_geojson, "rb"), + } + initial_name = "valid" + # first we need to import a resource + with transaction.atomic(): + self._import_resource(payload, initial_name) + self._assertCloning(initial_name) + + +class ImporterCopyEnd2EndShapeFileTest(BaseClassEnd2End): + @mock.patch.dict( + os.environ, + { + "GEONODE_GEODATABASE": "test_geonode_data", + "IMPORTER_ENABLE_DYN_MODELS": "True", + }, + ) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_copy_dataset_from_shapefile(self): + payload = {_filename: open(_file, "rb") for _filename, _file in self.valid_shp.items()} + initial_name = "air_runways" + # first we need to import a resource + with transaction.atomic(): + self._import_resource(payload, initial_name) + self._assertCloning(initial_name) + + +class ImporterCopyEnd2EndKMLTest(BaseClassEnd2End): + @mock.patch.dict( + os.environ, + { + "GEONODE_GEODATABASE": "test_geonode_data", + "IMPORTER_ENABLE_DYN_MODELS": "True", + }, + ) + @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") + def test_copy_dataset_from_kml(self): + payload = { + "base_file": open(self.valid_kml, "rb"), + } + initial_name = "sample_point_dataset" + # first we need to import a resource + with transaction.atomic(): + self._import_resource(payload, initial_name) + self._assertCloning(initial_name) diff --git a/geonode/upload/tests/fixture/3dtilesample/README.md b/geonode/upload/tests/fixture/3dtilesample/README.md new file mode 100755 index 00000000000..cd862833e7a --- /dev/null +++ b/geonode/upload/tests/fixture/3dtilesample/README.md @@ -0,0 +1,2 @@ +Example provided by CensiumGS +https://github.com/CesiumGS/3d-tiles-samples \ No newline at end of file diff --git a/geonode/upload/tests/fixture/3dtilesample/invalid.zip b/geonode/upload/tests/fixture/3dtilesample/invalid.zip new file mode 100755 index 00000000000..e69de29bb2d diff --git a/geonode/upload/tests/fixture/3dtilesample/invalid_tileset.json b/geonode/upload/tests/fixture/3dtilesample/invalid_tileset.json new file mode 100755 index 00000000000..a76162d51be --- /dev/null +++ b/geonode/upload/tests/fixture/3dtilesample/invalid_tileset.json @@ -0,0 +1,3 @@ +{ + "asset": "value" +} diff --git a/geonode/upload/tests/fixture/3dtilesample/tileset.json b/geonode/upload/tests/fixture/3dtilesample/tileset.json new file mode 100755 index 00000000000..e9d6cbf243a --- /dev/null +++ b/geonode/upload/tests/fixture/3dtilesample/tileset.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 1.0, + "root" : { + "boundingVolume" : { + "box" : [ 0.5, 0.5, 1.0, 0.5, 0.0, 0.0, 0.0, -0.5, 0.0, 0.0, 0.0, 1.0 ] + }, + "geometricError" : 0.0, + "refine" : "REPLACE", + "content" : { + "uri" : "0_0_0-1_1_2.glb" + } + } + } diff --git a/geonode/upload/tests/fixture/3dtilesample/tileset_with_region.json b/geonode/upload/tests/fixture/3dtilesample/tileset_with_region.json new file mode 100755 index 00000000000..bfdb5f53db2 --- /dev/null +++ b/geonode/upload/tests/fixture/3dtilesample/tileset_with_region.json @@ -0,0 +1,21 @@ +{ + "asset": { + "version": "1.0" + }, + "geometricError": 70, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 70, + "refine": "ADD" + } +} + \ No newline at end of file diff --git a/geonode/upload/tests/fixture/3dtilesample/valid_3dtiles.zip b/geonode/upload/tests/fixture/3dtilesample/valid_3dtiles.zip new file mode 100755 index 0000000000000000000000000000000000000000..93955274f1e22d02cddd40aa8bbc2df9a27403ec GIT binary patch literal 1333 zcmWIWW@Zs#U|`^2SUYK7#EdHzrRhuz3=-@N4Ezi-36|UTN~eh;(R0rr}y?fB3)&wVGi3IzAt@r zkkhX~ROpGsEm5ai_1E@an15xXVZyqym06jK^54w-`7G$risGuv20i8MZHf2HdNyyn zJxAfqZxvtRX^Sph=lta6=x~Eo^?u+Qm2VuW5gj^zo(NL&oKkQktBEgxe|)DO`x;xO{>|M>6Ht9RABe zdg4L%#Q8YS}>O; zPikN6$v&sGtzy>6l)8iG9|y0^>`?xvY~FKo=hBiNOC`!3-)Yn=_)@LF@9<9hPr%E5 zhj|S)VLw)0n%}sGp>o{^@1^^hDw#h;J>2dUZyPQj{&Cm7q~L2Zr*B0^e%8(J-+0z- z`|XbX%KJs{ZJqP=$LnuZe9{%icmCR`elP61SWWEi`m@RNW4=BA!5aWiuZMu?wed>w z;?=;^8^p-KzynOLC7C&?#i=EFS;hHz;KaHHm{36&EwSeI8FC*o5NQ4WudAR~TIk?W z-zBq~Ep~K>L??4@vD~ptY0AIy$y+9B`964XuX@k#ww9gj-`53;-RR>@EI;KPxcp)T zugY|RFNSj;Z9Nngc`4Md<%#j6?wiU-?k^Ln{(NECzJ$x$vocvfzn!WrTr=D6aQM+s z9rtZ&yG(YUtu?-r%J&&|KooZ+t-IL+<5AS{h4_s9o>ii`iO11@nM&y zqMY9&naTVJ4?hEXSoNj4qbtzEUO>zV^l*@?ql>SrUTzB5w;zB$1z|Mb7O(XXyqCx`b9te2b2%<3x4pvCPf#xz~LowqMVkj_b8QwN- y0G3Lqu?sX2OUxphh#6oA6PGkD1eRP#!3Q)BOV9;)v$BEw!wiJqfV3tHhz9^W6%+#i literal 0 HcmV?d00001 diff --git a/geonode/upload/tests/fixture/invalid.csv b/geonode/upload/tests/fixture/invalid.csv new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/upload/tests/fixture/invalid.geojson b/geonode/upload/tests/fixture/invalid.geojson new file mode 100644 index 00000000000..c59aecd3f39 --- /dev/null +++ b/geonode/upload/tests/fixture/invalid.geojson @@ -0,0 +1,11 @@ +invalid={ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "Dinagat Islands" + } + } + ] +} \ No newline at end of file diff --git a/geonode/upload/tests/fixture/invalid.gpkg b/geonode/upload/tests/fixture/invalid.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..8b7194d141370ac5344b26b478e656e0675e33c1 GIT binary patch literal 122880 zcmeHw34Bvk`gU@YG%d|SK`2llmq2MN=}v*NU)r=cp-oGZ0%b9z$xV7`mXf4Uz=g7i zj);gG!3`C6M^RiyM{(D2!xeWKb;b=HbzGnY{oZ?W(-|qz8ljglA=Q-y& z@7b4_7u2vphIIP;9$Szs6%7`N#iF?+DH4ekAY(xyFYa$?aqq|H)DlN^Rpe1w>7LI*kWb90mdJU_fyfJr!0EX(qO2r z);Ewj@y`>5kwebYSJgG>NrTC(Z?KScroP|HA;AZ!rz&W&p>7d;q1s>~&H5UBg@tUO z3}$^UHLtG0q9b#{@gjX*GH7ddGh|MmIOODzt687h7GwgX-RBJi{WjJcoKEtoM9CxV zHm}zgB%2x1%>)8uu*K#ji*xeGMYJAbnrqT8Dv0qvlPu1gLF!GF8;xpt)&ih$TrwKh z`FtL$n_11ct?g|Nh(~d5V;_WQ{vod0M z)Vvx!8UK{bMS)noHV;Eu^ouQ|sSc#ErbZVPQ-jH(=SqIhvzStn>}d|xz0mOY7 z)jr_@nfO_SW*PQS*kH;Ft7FW9&s!j1BTyn&-mf`uKBDP}HHm1_bOC?B$~yYyu7~wnJNo1fr(N4uYwJkl7qor-#^!}9=n4F0 znGu)IC~Qa`*E;p)a5%kvb2rdt3$ix1)z3JsfwciLm-m=got~1pv~0jR49xejhMquzYulJa z9+KI{I?`e=HF2e|r`US>s-kil3M&W3r7{q0ebLd@L3__}fFcr8X4~Q0_Gl|3R=^MHzWyfxXazuuM%X(^e@r}k@0J(lTD;k$CchM^OrqP5vGbcy9Wle9|V$v1)AnBW?$fIaO>nB0b zTeMlZq7>^#$2y?ZV4W=E4?hT7ug<{)?0&W_$U<)zwb+rQ=j7<`VaK%#eG7x1D-K-CaWb`QPvu%JP06gy={JWXxxg2M$VXYZWZ+*78-C=g z+z2+68(VFZh*8_SDX-T!;@SaZ`*Av=uc^fO2)7;yI%7#{)aW`<=kvR4UUnTcM-j{D zsb!-dLBa9)9ne%IY`z_gll8JFh&XfP?GBPZcmz0VwGv!u`uQT2M0vfc1^&P<%m8Kp zGk_Vu3}6N@1DFBK0A}ES!@%aDoj-M?P8AI!wt7U1s?FrI$z&nfXsLw>O)o>1mYDSQ z=IXOL>Kp2e73QVdgkH3|YJFXG?_;eF29b@m$hWn)eA;BK&Rh>`C3S|%NC;Zpl+vlw zic6>JO3J4dl$1>^DJddK0^R{|ei zpFh}wy!0@`&FTR$#gN>^2FNFUBoIAiwnUVsp6{7peT+^-4KP#6v9?SkpCuZWs_`_! zL^BNS7zYXSI4-}ft%bFd<_5E=&ZKXTJnQ)*+7lWtvR+uzB4Lt~@iSgKL&Eqm$at3i z9C^uSh=z$Zt-UCAn;+KVfV~LF&pm~))S}X&#Ks5w^2XQK&JzSQt-Pq13+UGr0Avw0y!~o*k<1UPeO?!` zYpsqql|BWDFD~wg&%#JifEiSn5Nf zOPTYZ_J4D8H>Is3?>{wENSUeEg3YzBoYgT1(j``ZE`tB~g&DvMUEvU|Nn_d{YjKp{3m7rGk_Vu3}6N@1DFBK0A>I)fEmCHUI)kc@%;*8krYsozfaJ*>wJ zUn(Z z_`_Z>1DFBK0A>I)fEmCHUI) z@F!w``~5#W{{ItY7>+7t05gCYzzkppFawwY%m8KpGk_Vu3}6OMf&sk#e-fnOuV4l+ z1DFBK0A>I)fEmCHUl-Y;lE!eDzax!CwLEJ9 zSm(H8G_LdcJXSZensJ}3ZT1PFheF;meNM91&#xr(Ye9t6{c$=K5 z3X>@t0GTIgk-fOqKbAfAMvOd3`sOH8GR^s75$km@9f4JDHpp0Q?Li;h9w*5Y`L zYEy+mo|&I7KD!*%N%7C)KPw}}pPE;rC*z-zxhN2;*XChJi+-_%G}VDL*3{^tk_x{U zqvvYmo@X(&RkEio4C`tMl6@YdRv}zFCw`Xfi*0NNl+O-pfL+Ism356=f_kD9ez-RA z!-_hS*#cF&$wIcZT7y9kS>Ir&r5c*Z`TC}44s=mvG*1`xH79({^IWu~{USXHK0e-m2*> zuiX91Owhm9YWKO@J>CGy{&;0--@K0Zl036@;gAy*?4Ce^YulJa9+KI{I?`e=HF1Tg zr__1+s-kil${Gj8r7{q0ebLd@LAQzH07WFG$hE__?a@}!XfiBlg#J=*A&l`4MU?aJ zjm`Kd&jTu%@z!Lw{(4(7q@`popW3f2^<)Zf5!RA~HZjPMDm0AzAPa*E)Zj*vo|B_<+X6wW9YzN(Xy0HoV=>hBws2&`V=fFb zoI%#Z&WODT<3|ZxZzP$g&o%} z^eqg2t~hWl$H~;LJ(X*@G$pe(r{5foskF6hxdk#@k(M;c&dN2L)^s$42 z;AIjJmiD6fw#>#xaVLcc^9TJ5W9|Fng5k=(&E^NgABdl<7(%rJ`HD8z>w|LT>*=^e z_2YD5WX?k#M7%3=(j@Vw?A|P9I-naC0CvO8ZSTj-n0TP9?|s;(7UUumUYD}YY zB({GYu>1Pd8BuMgi?Ign4B5BMi)k=*WGr}I-o~cP@RY%fWW%%Qp7P0#b`yjVKi-9z zm}pggT^_+fIdVg%UCfvncz)J=={;>W*P9gLfSX=x|3(UK8vNbs^A zUe@cgIvTEyf5&CL86)JG#l>P+*5g)Z`wmF^YBFN$ioPDSWG<<7vJP#c%7LUBEp-MH zgi))Ho&_bf+~k^;tcQoNY^<%DXN0w6LuF4OpfgljVOgM-vQW`lQ(diIi$?0X#YIJV z;IGDD(t{UR3a^1r6?K(nsw(PZZKDakz(1|D80zb+wRHf*jV*JFOHmN;C2LJBuPZ|; z4yRXF0#JD}7}!hH-*6Km!`0pDAJpF{w<#wo9#WXnKTAI=eZ-)@4ysOjJ1v+dPu-I` zRsNE^MJ`RbGNnv*P}VAwNJCPc7*vM+AjaS>qssKUkGY4(! zVEoRI4wmHR&wXt^SUVO$e9m^4iSn1k|JGv+oxst#5b-BxU-CREuxHkU6Dv=PKz z!hy!4^k~xh(l__+XYRf6tFhEm@7~k3VY-;{My~Od^x^gq6W$GEfPkc-;hG!hYn3MpcZNg=my_adIOLwS? zPJzUXTVC&H?7m>Z!UA$3>vr3SY{76VNd%Xj0B*y?$8Pxdl^W`{u4UJ&XPPOp$;A2%nhff z7N!B+Zk8A)7_4P90vneAY)|9W^VjXtP=9Mp+usOaqvs%NAk78U1q3M=$(fa$zQIpV z-+w7WTJ-l?HVp15=P)@f#HHR8UPx7AEBQ^Tj#upS#RQZNQHENN0iKeiuso~5B?>R!8lf0>yYHgzH6 zW!AAaB2zGo^OU4HGuZZcV99z7^_PABoU<5UPMK0~gNjhI=7|S}>SnFajHyz^v=n zZ@S|!t)X(y-1h1T02^FfZ-WIPAE6SA(yC6Hq{H`Ye(2t#W2tGb%D1|pCQujE0&TFK zQt9)<8zB=6=1?U~lF0dE<+tfjgaU_J#sQeBaGuW#dnF0EU=&t~P{t=9?(m$wk6a_t zP!Hv(@7Mt-$^x*u8ph|f`5itYMKBbjKv0RQo*id)FEM0ls5Yy-@^Pqo6#0$*c2~P? zEs-i1rX?N0Bx>bI>B8-+G|;4O{Lp$Tz@+PHY@W7umzOh#G{H#rK?o^P>vQn==eJpO z8Y-pplHxC+k{dK>0cjzp5rYJSxYH0wUZM<|GHUJzGd0wUKbrq@AyhwUd6b8Nj+GmF z^<4LqPzXkANli-L+?~%oGHV`~gKWt5$$*xcJFlIy1-~!I5=y~H4mmd_N)@t5XPBX(V1(5144|Y;tYkXnw8)+SSz$UwH)p$+D|$o@N{5k618Fi!^Ee?3tr>bP4fUo zI$@p-wnCC7=qeB*!APtGAtkC9c0Bey{f1UU`ENUKksOdDo%M--77WD@ zNx7T*>08@oj?z$C&3D_LgRv{2HF%-&_Ojxi1j9JR0Fx$8WDEPI9wn|(K|{X%WBPJ{ z88?p&*cq3bWyB|b83!zA*eY;NnGep;iQN5vV)Z8?_>W(h0n7kq05gCYzzkppFawwY z%m8KpGk_Vu4E(tn$P`P8iN5GC{x_wYp+G}af)T62A zrH+%|E!U^KmEx8CB-Qw&|5X~^%3j(tcBzJ&ecU#$5#fZ-9K_=6 z8@*)nk3J3c`NZOq0>Ck5)uPK!Y;dpuc91a=;sfD$E<<>q2s#ZAi&wu{S=Y1@@b0_g zlU&%6Vi;A+1_H44PD1=sI8Y0XfW{;Osy{yBA-{%Nc=fI6cfn2(dSr#)XJ>*zn}qmC zIFh>>A!R2(Dq==zH=nMdUa4)SO8}{QM7_@+^LF5dRVm=hP#(M0Q9%`!7u)>}l?};@Ss_0Zu=B z9(TC~Bdrr%nUpPfW9l!Y-|I_(qifW-?D$4~>K7YH1 zAnxr4E%EuGm2;y~77KQG<)qaLZdcV$sfd)^g{bN|Ji$qf;(d z!3mYwODhUYHrR6*^m$pX3L%K?g7Mf{2ya{hylHnoQ?+s$Njk97wmsFw?n2~DHzK$17RgP z&$eRuyUvT>18dy+-Fqu%n5h{HBUlyO{Wy?oR|tk{osQrV8Gq52%Bzn4TSML3d{*8V zU~#%70v9&O%LRkAlq0Z2r`iS|+mn;|p@zEsO>1re@K|nM$a;JZKQx&HahYJG)@ezT zwc;H6n7~&WYL9FGyaqs;)_{z09(R7l3+8dDU@-So1Qu>xVl6+{EPMQ`bdsuk^Xmf- z0@&0A6^!584mb6{2_sVhL0l{xZAwzK!K-dtKV%q5E&fNzS9b#1l=|>trYaUX1Y9Y* zNHAJk8A2;eP|6l(-uKU|H6-=jcJmc)09sjH1=Jm^*G9sA;(EqzZ-M>9I|U;;CnLnH z1c-BWuYEo=m!ys?xc?u8Pyi;I=Cudlf?}}z3kBn}mLi-)mOu9;XX9r&k{Z0Y{HE1_ zQ(7D5vEJutV}QmTg29|6NfVX&^>CW4A*tDyH@@YB%CW>)4OcotRZI})3&wF3Bb;yt zF4h$H?KSPWZLfy9YyG$9JPAAVi}i2@pc(qW0Cb&TAZJn11Rb1Vy6~-AG}OighOI0d zpD3a#SP#pcG$8f}##vd2aKb%-*aVU99DDz|{Tk~1i?;qqz(Kddj0!(og~xc;K?_X~ zR}02+6dowG}J0JP30}fCW3~qG5@nC|uMmS1-Qj`_9teAY)B^v7D zA5xYtfGo;a)pIw26U1J@C{A6{{Fy7Red3nQkd_M``{)p$=oE0)$+m7CaCW0$9LJ=j zsS#bbbGkeLhfel>u=okUnKY=H^}$Ir;%31p&Qp`7rfv6{D_&zX)WeTv?ven?sj0O- zs9skC&aHxR9C=Aovti4U{rAENv2(tA=XyH~?DFI_Hn22;xJ@w1%3Oq!$foyXTt8;q zVh#1!_qnebU^tg6gQG_j6jPrIj@c5o3&+YyiuJYq`Q`V(@%Yp7o_qL97-!{3IV|oZ zId^W0xI-{jc>TYH8~;yI9Z*@6UnoykW+?V5W~CoWccn`Q?H*K`_Iw%(=l`!vEt5Yh zZ%X+lWmC#n*#k0*^c(3pQjO$3iHZ1-SVs&I-zuIhdR^3#Xab;h6_+L=5r6yEf4hUI z*HBY#o!$B{Si>su)N01x9)Qz{9uFfEPwgjBW#PeoSX`TTW*p;BlQPl1jY3kRZQ;wgfGT75}Vb+|=z!I2djYNhY%*Pnq@`NU-~tI!T- zo`R4tnYc_a7~@4?iK;p{4ZBO@g_9@lj=L3*DzCWI5P(`pCN32WtMYfA z0rLrWJ#xgI2CalgTnxiSdkZ9|SU8S5DURsL3l>aVt)aHLKiP66;JC#_1~{n7LVAh> zMB|HYs^v3r7b-Q*q#oF<-v|K&&{w9y-aK&E*Tm zakV5((BV~o`Ko>^TwyTv=-h?S;c5}<=CN=((ZkBbI^jSr1TCn`maZGl{}M?8!lvUGSm%Y7O=Fi$CUbCzb88Y>FGNdl{iA zaW^B7`~*2tG){A|k z-EeoWU2wWqS`l7e0=&cN7v=4`72Q7bR4t^-DjT<;pn>5g#9&?=(1fPVd0J8qi(Z&v zdUTP7sz3iSw-vBXlfj@l(iswp)3O}lWG6@yT$->!0iF9(;8ueh0B5;O(~}&SR1}J| zVi`gjmI&#>GqcL!wvmRnvzBxL(lXgtGjvXbBG{H9ghcfYTtakY$ORheAOD&&{6|1o zDjQP?HZIr%m?a28lOPcXZrb}|@kZ#4JQS?pPAM;ujkfr~%G@vuD>NlcQ&Jf2_NKwh zHQdz6!Jojgn`BwsIb{!PX9Z_T%VGqRDD$C-TRhaHp(YsD+eMI?#j;WL&<}z68R+u} zujOoul4cFuF|qzixR1g8N%uvEAVG^{BddIEZGv;(yAVMntPRYvO{sG=)INX9%ew$# zp=`t=TcCyYx?rX?e;(8bt8IRvdE{tBcw-Wn!oJS~uWx}9-h-e18(9i?jk3%tm{EWM zgy4FBu^^O02DjpGhUbTzs-bSU{D~3YLmOm~4XW;Ij9?}uNX+3`YmaUi zub~{v-~HPtfHBL4nF}iXEVQUlqjJj&LYZS5I52T|zB6{+ZcojyBTvF*VKBZax*vL0 z4XuI%U z=c-3=;m&DX$_7Gh7Kb-j_Mqu z6jc!f>dawBS0i*|}HL#rm$zUF3If0u@oq}>3|*q(BirBt{klLKPNxm7j_nk6a+fL*#K<%l2W7cp zBDZ432ZdA5(@@jL8*SV?qv%5Mr8UqO3Htgm;bMpvzd{?gi=}6P7Z=}7c_(+*5&`p0 zTpZY#I1Hxi-?hSpSnsxo-g=0O!w&H!sCwlR0vGZ16BCXhzEY1v?1@SI{u7{syO4<> zP#-fsD)-zFx*m3*n3@NFcnQ=Wq6@?qFM&%}`nyA>6ym^7f5l_o_p3l zowpwPu`@rYyG8*M$D(cG3oC36pWkcyg;)r)&$BwY`=25_#1@gZ>%_};mBQ+3^*@i4 zaQShbct?#7HiP^^4EU@_#zS0IxI4Lfp$MWpm7A={i76mOp;PZC>$KcWZu@=E-x2*K zYb7`HlA~{TGO#*C&TEIF4nv6Kq|7rHhbs)&F%y7>-rC9C5CsO901kHZtQuOag^rGs z_=(BUxZ7R5$rBxqa9SsKD-=PDNq}(EH?P!|Qd;VCTge&2V9H8#PL_)s;lr?d z-~f(J?lLHX$V&jQ@1LK2KYyB*ntbiuKmV6Y!qzNj1b{?Cl%$y#pALH`cN-Kz=f~1c?kXrSzyv7gxIdXRqCiVM|NQe) zlpI@IvK$M2?%=>fw{~(jKqb!Y!H4q?UNlil73|panu`m4bCw+zH*J285PMqD$=&<} zEntH9?W9nT z`};1~ZXw#3)yqzmdB7N_b&3&2BBMR}m}9y-PfNWbdjH6LSZfw_X02GrLOlTU;R6O} z?i3+_>;#E`oEYpp74cMZj04z^W#x>v$_77^Bc$jpI}0$xL{wLH=z8F27R&p^SC zOb~gvE^+3_QZ2RDH20G4Aow$~mg(DJ`f=d&FF!qL+@ZPs@FNI(qx8wZ>bYj>^sJ?| zuw{W;dmnJzm#t4q{NO1wm69SYb?Lb6nx&MPTA#H9c6`9f@xUX0+By_^qAELep(=lU zA=qYZbMQ3?eO*=)T###Xw+S(umbF~?;zS!*MPGe!SX%?*s*g^|UJpfht-Auco!ovE z3GrG|dV2Z-ecF6c2TDqKlGGa0*wyFkId|4murpyo%AAqVUG9*-Op{zOi1cUsyK=F7 zz`bJM8We*>JMKh7PO-i^3@!nNO(m~Gr(=zHHivJ9J59MujUC()$OzZwA+ zC)glS^u(Rpjc_#{>;nDt6#!ffH&I6b4Q*}QPA(5%&Jv92YDbufmcc}6e|aP;UqemK zm|}VhFx$m5VQqp51YsF$reL6!AOcFXbq3a7>z2UI%!0eFyww5Sr=WN`x5pK3qlP3+ z7mUOP5K^LTaC2vWddy#mI*S=Y0VyCZxA@_AAboy6EQOT|hHCXAs6I) zfEmCHUrzZ=1uZiG4eqjbM1DFBK0A>I) zfEmCHUiK1~zw~R8J7?RM6wq_p7_q->Du@f1&=D`d!su z)vu`btDjLlqIy#Ou-dG;QGJKHT-~DHqu#B$K-I0@raoKsz4{FG8r4T?uj-K6s`^ab zq?V}XtFBS&RhO&hsBc!6sCBAKRR2+rRgX}sRqv_ORL`kiR-LQbq&h>jMzu=Ss$x`D zRg=oBs#R61DAf#AnW|8gry8#sqsmlesM1w3<=GNimndA@S1vQxQU*{<{{S*2aMT)9ZOKsjHjSI$wED@&F6${gi5 zWtMW7Qmsr=N)#s)KPtXae4+T4;seE7idPjcD4tS0qPSmir{WgHb&4w#mngO?wkkF# z)++ppR)s^cT(MA5r=b5tCV-=g8Ndu+1~3Dd0n7kq05gCY_&;Sp+Brlb5);zS40w}C zI|ql~)ZsT(_)Q6K5^1L*{3#vYq|(ko@CM&Y3%{j?-{j%9l<=D@{3Z>*Ny2YL_)Q#s z6QQ@Wkb5R_&p__!$X$=zb;w-{u1wm2{$2yFRJt0u?Z^!xH-KC}a#w*XleQt(hg>gm zJ;-$;7X*m=Bucd?lk01MeY>jmLYdIxH72@ zxjEoUq}j;j>`5x+tV1g0EJiBjtU)5>Y)LBR3`;641(%RcMy>|A#mF6l+;PawNA4)( zW^%52fpk2)OQj`9SA=v+Mbbj_u>gJCDUzkB8Ig1ne3D5|7`km%Y7V)K91mA^%ET06C&v__#~5_ z!nxeXJoFJ2FLk#_%Gtd{$`vfB`bCjcib|9EP56r|Nm4FQDOZZnUtBqoO1WYr;T~`$ zNFwD5kc5kj7{Y_snD86-Cw~2p8929IA{AXU0Z>bM6?K44U8$++jK~j0Wmj3ag zOF?!SL(d%od6Us9no>rRl1jiE== zK@_}ROdoAe1sP-vUGX-^JG>sk_?rP_zA^OhD3B~(PtZqS{ut!n#?V#EKu+WJ>GaX= zZ6Mo?p$8g3%)DMg9~*ub$lb=!p6MVnc)gT9W_5zJ7(>?%0~yZi2hqox_k!GD4BZFM z?pw+0W%RL2n?RNtLwi34`4_KGp^v?70D*YkPytfO>(l6C|Ly`gZVcUa0my~CK9%mu zI0EvOF?91@kQ;ctobDQS704cA=*BBSuHyBB>8@%S2;|9)UxNIH*JsdOXF5S3jyKK$ z;ckiHUfFclc^x3@jG>*|K+X@VMHA?*ogxsz7&?CiNHed`p}VfU8RS-D=xPs$m)B3E zyRLd4qLU)gv05TEcascE}UY|#IkGUHJ?Bv!l zAY*y`sdRV2bs*OpL;FU9jN$cT>27L2$UVl;_3a>Qc>N^0d)G7&$isWJAme%cP`dlC zAA^8B+|UHFgx8-!cfb7#$ZN*XEsugc#_NaC-Q80`%8jAF0?&Vq@T{YcXDZ-bM516NUpIj~PSf&H$Ok>nrHvU%w0TzA?1zC6JeSeIEw_RAdH?h2?(>U4 zN{pe~CxB%0`Wm|X`h1W=$nzjbJFl;$yI;rv8EOputp=o)*PG}QUmgYNGKRMN2y%?q z*U{Z&w}b41x&-R1N9UYdPw-XRXx>XEeZ1WQvd|cM5UPp?BUQx+`uLV25Qy3%kjzIu zu)@4W@&He=}7;UJm3KA%4R*Bu~0-(7zJIgi(8(Z}!E z1+v>1dJ(ev#Yk3bI zSAbk)41KBq;m+o9uOj-y!skF*P$wXJyMlUHHfUMLDqooeUJ}$|I_H71DAn>fJ@-+m56DV(?1V>0OUbq z=;c))5mTK`|2*|^kSBr5&pT3?ydY+t}5y&Qi;RGTFd}u05kAEXJE91>tjgOM?~r$)ZOZD;1B%53}6N@ z1DFBK0A>I)fEmCHU-J{)*l5_pFgn#3+VMKb8@xSO=wtPlg6uMeF1rcjW?tV& zAGhD2*468-U=@YpQ5GU*jC;^$w>rbP*9v%b&3z*k- zgPh>?R{F%UM?oG3ofpK%>sQd-7ZD&*2>VTtw|IRs{nLRg5ZI$|-C~f)y1j!wro9N{ z5?DLW138uV&(O!7{Q=~tF?6{OB%jwiVXwm+5DL;d5hRz_yXfx4auC=zapjF55k8ac zD>$d&V6VxwAba_6@LLiFde{GpcprHEKTHH({}1!|^ zf$Zn)wuAnu_y`DW*?Hz!kbm&{b%u=fZ-KmH+^xD6BvK`;H)L!W0W!+Cd(fL8kt*$U zL&nt_5Yo6?au>+md^l$qGIl)+@(*L^=j%YO=k;eAGA;y+3t@}Wu?mpLR<^SY8LS#) zurc)g^B^zq{x=vh%!MGu#?XJB0g1%3(~vRW2GVQ{eQpJ@@%}d&GN#=Na-T8uF*Fe$ zKgH`e88Ql7AgnR;K|9DAUccFpG5Jc6t6}fOERZ?8ev2Wa?NJb@+TVolyt$IspKZvv zi3EZDHHTgWiCi(Y)sXR+6=VhIAgn`sc>m`ZhNSHVxdOs~?Qd`1#_P{D44DS{X`nv@ zTbd57;`M(q3|T7!fqfjW0!Ob#_TB8Hf68nF@f$-gWP?PyRQ%UY{;hZYKhio%&~8c8 zN^$G|J*^aiQ><96A-^Y+O{5`Pmj zfEmCH42%J8{71sP@x z-Sc0N@A+UG=&tRBATXYJ{Q$^gyuOX@y6-cPFN~r4H-T*C^$X~($6O%LiGJxqkjPPq zdb;Zb0|NW-pMmkfGm-fC=x(_K1g5a=)q_;?;rv!dXZjq8;KNULq{d5kw{?S@fHc8z zmHQ({TZA6z@W6E17a(66LkD0_|AAT_H`$S$WJf@f9p!P;-FFs)l){+|m_~f!OFn!X z-F??~kR33H`721|+(fdYB&~G!OILuvd532o26==JC)rV(Ng+&$7LWe?Rq1Op>KSE z4`&&D{L!aDo(0`2Ag}UzI}Gq#;X&SwpMm}hUcVL%{<>@x)ZlL$-Sx#=AnzDM zH!&a)djO6re*`%OWd+98_e73fT|jrA0eO5zzA^OB9U%Mo@OQwms}mqUgWrFHe8TH5 zq>nc}00Ps%_l^gd!0Yi_5(dt#m-1EGDZG#0>y+ji`snQ;5SXpM{dSN$`1m9{rJ3wB z=4$%b5Lj6l0>^1?zYHW&6q22~Y^RU4ybA)ydv1s6s@tFEag&|84ARFwd<6uKQtgMT zcYmbn{jE-82I#Kwmx92m$Ucbwz7QXuWT!O!bl1XnKw$o5KQs~hBTdA4^sy@*1%WB7 zJ0YAqBjNDD;Wx~{|DOzSIl=0HAEpm7eVNvg^Mo-6e=diZXgiPZKDSW7OV^(NBib`)s27-&gTorN?t*3^*m^wkDaca9S{rcZ4O(IncO$F!f4=w ziSW=^U&(RUFAqJ@CdbVpP5MRQG?0|3lFUUP!yFmRh@rX$(rmFB0(wtdaBbA{yu4pS z9*~~f6AxW)Eb^<5;r!iL zF(F)3(gfsh$#0XtCGq)NMe>!x01M6UR%T+d7%H4i6UHMJx6Bv7V)1XoV)6eai^7V_ zAKuu{(xTRJi)29A#CijaKlp2NDI7{9lOh!L*E;5UnKgZy$AMK?k>LNR)lFyNIC*Ac zqqvg_+M3bzdajdG z-(aYv8k)%Y`X*i23+ExPb37@}T(CggSv-IkbKmGczMPLyLSoB(q2IVd-^G^KnWK?s zn$6?_0w<`i~~^GEzu9(KocY=q(|qTtd9QyiV;{d1iHWYN!5}W7=;V`N%Ca zG*}v`8qzX=tk;r_W`n7kMB(PL4jt)Xy&d4%Iyjg0u0?;Z&6_nQC3C?fu?YR1SYVYK zejdzfYY+O+XDj-_v7Vn03s{Te6eBv zPpIn`TXc4xyWQiBQuIWmSJ%_2);Sm_>t%zi&x^j@`ygQVwS`0M)m7A)%$5cU3Iyq5 ztU)_NHku3z8uihU(2-FQ(2=p=d3hTrXUVf#Vf?X(uVMK&exzY_GPYp5pRw9~?cQL5 zg5S3)h_DkGiUM-Pp{hUu0b3+># zkGOqn7=Ki@<$>R@E~jtma`;}@Rqc$|&cuBj60svxO~gIS<%*XMzM1#`0QQp^_W%F@ literal 0 HcmV?d00001 diff --git a/geonode/upload/tests/fixture/missing_geom.csv b/geonode/upload/tests/fixture/missing_geom.csv new file mode 100644 index 00000000000..702f74064df --- /dev/null +++ b/geonode/upload/tests/fixture/missing_geom.csv @@ -0,0 +1,2 @@ +m,z +"14.1054722222222","41.0927222222222" \ No newline at end of file diff --git a/geonode/upload/tests/fixture/missing_lat.csv b/geonode/upload/tests/fixture/missing_lat.csv new file mode 100644 index 00000000000..1a195ca2dee --- /dev/null +++ b/geonode/upload/tests/fixture/missing_lat.csv @@ -0,0 +1,2 @@ +m,x,z +"14.1054722222222","41.0927222222222","14" \ No newline at end of file diff --git a/geonode/upload/tests/fixture/missing_long.csv b/geonode/upload/tests/fixture/missing_long.csv new file mode 100644 index 00000000000..21659c52d77 --- /dev/null +++ b/geonode/upload/tests/fixture/missing_long.csv @@ -0,0 +1,2 @@ +m,y,z +"14.1054722222222","41.0927222222222","14" \ No newline at end of file diff --git a/geonode/upload/tests/fixture/noCrsTable.gpkg b/geonode/upload/tests/fixture/noCrsTable.gpkg new file mode 100755 index 0000000000000000000000000000000000000000..9cd278089ac32877454e6be63524ad1097ef72f3 GIT binary patch literal 106496 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 buXqdurm*gWaPEwT!v}}oFa!U8GVuQZz<{i> literal 0 HcmV?d00001 diff --git a/geonode/upload/tests/fixture/valid.kml b/geonode/upload/tests/fixture/valid.kml new file mode 100644 index 00000000000..f9913584933 --- /dev/null +++ b/geonode/upload/tests/fixture/valid.kml @@ -0,0 +1,37 @@ + + + + + + + + + + sample_point_dataset + + + + 1 + 1 + test + + + + 120.89945941369977,14.358088895403743 + + + + + + 2 + 2 + test 2 + + + + 120.94212582236852,14.23553050541452 + + + + + diff --git a/geonode/upload/tests/fixture/valid.zip b/geonode/upload/tests/fixture/valid.zip new file mode 100644 index 0000000000000000000000000000000000000000..c8671be0eef4dc047c54ac897548d0928d373df1 GIT binary patch literal 13924 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 HAJYE;w5v0> literal 0 HcmV?d00001 diff --git a/geonode/upload/tests/integration.py b/geonode/upload/tests/integration.py index f7c7a40c581..4d9877f3a7c 100644 --- a/geonode/upload/tests/integration.py +++ b/geonode/upload/tests/integration.py @@ -36,7 +36,7 @@ from geonode.base.models import Link from geonode.layers.models import Dataset -from geonode.upload.models import Upload, UploadSizeLimit +from geonode.upload.models import UploadSizeLimit from geonode.catalogue import get_catalogue from geonode.tests.utils import upload_step, Client from geonode.upload.utils import _ALLOW_TIME_STEP @@ -75,7 +75,8 @@ postgis_db = dj_database_url.parse(DATASTORE_URL, conn_max_age=0) logging.getLogger("south").setLevel(logging.WARNING) -logger = logging.getLogger(__name__) +logger = logging.getLogger("importer") + # create test user if needed, delete all layers and set password u, created = get_user_model().objects.get_or_create(username=GEONODE_USER) @@ -262,20 +263,6 @@ def finish_upload(self, current_step, dataset_name, is_raster=False, skip_srs=Fa except Exception: return current_step - def check_upload_model(self, original_name): - # we can only test this if we're using the same DB as the test instance - if not settings.OGC_SERVER["default"]["DATASTORE"]: - return - upload = None - try: - upload = Upload.objects.filter(name__icontains=str(original_name)).last() - # Making sure the Upload object is present on the DB and - # the import session is COMPLETE - if upload and not upload.complete: - logger.warning(f"Upload not complete for Dataset {original_name}") - except Upload.DoesNotExist: - self.fail(f"expected to find Upload object for {original_name}") - def check_dataset_complete(self, dataset_page, original_name): """check everything to verify the dataset is complete""" self.check_dataset_geonode_page(dataset_page) @@ -300,7 +287,6 @@ def check_dataset_complete(self, dataset_page, original_name): pass if not caps_found: logger.warning(f"Could not recognize Dataset {original_name} on GeoServer WMS Capa") - self.check_upload_model(dataset_name) def check_invalid_projection(self, dataset_name, resp, data): """Makes sure that we got the correct response from an dataset diff --git a/geonode/upload/tests/test_files.py b/geonode/upload/tests/test_files.py deleted file mode 100644 index 356203f1d02..00000000000 --- a/geonode/upload/tests/test_files.py +++ /dev/null @@ -1,43 +0,0 @@ -######################################################################### -# -# Copyright (C) 2018 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 . -# -######################################################################### - -"""unit tests for geonode.upload.files module""" - -from geonode.tests.base import GeoNodeBaseTestSupport - -from geonode.upload import files - - -class FilesTestCase(GeoNodeBaseTestSupport): - def test_scan_hint_kml_ground_overlay(self): - result = files.get_scan_hint(["kml", "other"]) - kml_file_type = files.get_type("KML Ground Overlay") - self.assertEqual(result, kml_file_type.code) - - def test_scan_hint_kmz_ground_overlay(self): - result = files.get_scan_hint(["kmz", "other"]) - self.assertEqual(result, "kmz") - - def test_get_type_non_existing_type(self): - self.assertIsNone(files.get_type("fake")) - - def test_get_type_kml_ground_overlay(self): - file_type = files.get_type("KML Ground Overlay") - self.assertEqual(file_type.code, "kml-overlay") - self.assertIn("kmz", file_type.aliases) diff --git a/geonode/upload/tests/unit/__init__.py b/geonode/upload/tests/unit/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/upload/tests/unit/test_dastore.py b/geonode/upload/tests/unit/test_dastore.py new file mode 100644 index 00000000000..361b67f028a --- /dev/null +++ b/geonode/upload/tests/unit/test_dastore.py @@ -0,0 +1,67 @@ +######################################################################### +# +# Copyright (C) 2024 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.test import TestCase +from geonode.upload import project_dir +from geonode.upload.orchestrator import orchestrator +from geonode.upload.datastore import DataStoreManager +from django.contrib.auth import get_user_model + + +class TestDataStoreManager(TestCase): + """ """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.files = {"base_file": f"{project_dir}/tests/fixture/valid.gpkg"} + + def setUp(self): + self.user = get_user_model().objects.first() + execution_id = orchestrator.create_execution_request( + user=self.user, + func_name="create", + step="create", + action="import", + input_params={ + **{"handler_module_path": "geonode.upload.handlers.gpkg.handler.GPKGFileHandler"}, + }, + source="importer_copy", + ) + self.datastore = DataStoreManager( + self.files, "geonode.upload.handlers.gpkg.handler.GPKGFileHandler", self.user, execution_id + ) + + execution_id_url = orchestrator.create_execution_request( + user=self.user, + func_name="create", + step="create", + action="import", + input_params={"url": "https://geosolutionsgroup.com"}, + source="importer_copy", + ) + self.datastore_url = DataStoreManager( + self.files, "geonode.upload.handlers.common.remote.BaseRemoteResourceHandler", self.user, execution_id_url + ) + self.gpkg_path = f"{project_dir}/tests/fixture/valid.gpkg" + + def test_input_is_valid_with_files(self): + self.assertTrue(self.datastore.input_is_valid()) + + def test_input_is_valid_with_urls(self): + self.assertTrue(self.datastore_url.input_is_valid()) diff --git a/geonode/upload/tests/unit/test_models.py b/geonode/upload/tests/unit/test_models.py new file mode 100644 index 00000000000..27687218a0a --- /dev/null +++ b/geonode/upload/tests/unit/test_models.py @@ -0,0 +1,57 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +from dynamic_models.models import ModelSchema, FieldSchema +import mock +from geonode.base.populate_test_data import create_single_dataset +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload.tests.utils import TransactionImporterBaseTestSupport +import uuid + + +class TestModelSchemaSignal(TransactionImporterBaseTestSupport): + databases = ("default", "datastore") + + def setUp(self): + self.resource = create_single_dataset(name=f"test_dataset_{uuid.uuid4()}") + ResourceHandlerInfo.objects.create( + resource=self.resource, + handler_module_path="geonode.upload.handlers.shapefile.handler.ShapeFileHandler", + ) + self.dynamic_model = ModelSchema.objects.create(name=self.resource.name, db_name="datastore") + self.dynamic_model_field = FieldSchema.objects.create( + name="field", + class_name="django.db.models.IntegerField", + model_schema=self.dynamic_model, + ) + + @mock.patch.dict(os.environ, {"IMPORTER_ENABLE_DYN_MODELS": "True"}) + def test_delete_dynamic_model(self): + """ + Ensure that the dynamic model is deleted + """ + # create needed resource handler info + + ResourceHandlerInfo.objects.create( + resource=self.resource, + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + self.resource.delete() + self.assertFalse(ModelSchema.objects.filter(name="test_dataset").exists()) + self.assertFalse(FieldSchema.objects.filter(model_schema=self.dynamic_model, name="field").exists()) diff --git a/geonode/upload/tests/unit/test_orchestrator.py b/geonode/upload/tests/unit/test_orchestrator.py new file mode 100644 index 00000000000..b4d3871c042 --- /dev/null +++ b/geonode/upload/tests/unit/test_orchestrator.py @@ -0,0 +1,373 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +import uuid +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import override_settings +from geonode.tests.base import GeoNodeBaseTestSupport +from unittest.mock import patch +from geonode.upload.api.exceptions import ImportException +from geonode.upload.api.serializer import ImporterSerializer +from geonode.upload.handlers.base import BaseHandler +from geonode.upload.handlers.shapefile.serializer import ShapeFileSerializer +from geonode.upload.orchestrator import ImportOrchestrator +from django.utils import timezone +from django_celery_results.models import TaskResult +from geonode.assets.handlers import asset_handler_registry + +from geonode.resource.models import ExecutionRequest + +# Create your tests here. + + +class TestsImporterOrchestrator(GeoNodeBaseTestSupport): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.orchestrator = ImportOrchestrator() + + def test_get_handler(self): + _data = {"base_file": "file.gpkg", "source": "upload"} + actual = self.orchestrator.get_handler(_data) + self.assertIsNotNone(actual) + self.assertEqual("geonode.upload.handlers.gpkg.handler.GPKGFileHandler", str(actual)) + + def test_get_handler_should_return_none_if_is_not_available(self): + _data = {"base_file": "file.not_supported"} + actual = self.orchestrator.get_handler(_data) + self.assertIsNone(actual) + + def test_get_serializer_should_return_the_default_one_for_if_not_specified(self): + actual = self.orchestrator.get_serializer({"base_file": "file.gpkg"}) + self.assertEqual(type(ImporterSerializer), type(actual)) + + def test_get_serializer_should_return_the_specific_one(self): + actual = self.orchestrator.get_serializer({"base_file": "file.shp"}) + self.assertEqual(type(ShapeFileSerializer), type(actual)) + + def test_load_handler_raise_error_if_not_exists(self): + with self.assertRaises(ImportException) as _exc: + self.orchestrator.load_handler("invalid_type") + self.assertEqual( + str(_exc.exception.detail), + "The handler is not available: invalid_type", + ) + + def test_load_handler(self): + actual = self.orchestrator.load_handler("geonode.upload.handlers.gpkg.handler.GPKGFileHandler") + self.assertIsInstance(actual(), BaseHandler) + + def test_load_handler_by_id(self): + actual = self.orchestrator.load_handler_by_id("gpkg") + self.assertIsInstance(actual(), BaseHandler) + + def test_get_execution_object_raise_exp_if_not_exists(self): + with self.assertRaises(ImportException) as _exc: + self.orchestrator.get_execution_object(str(uuid.uuid4())) + + self.assertEqual(str(_exc.exception.detail), "The selected UUID does not exists") + + def test_get_execution_object_retrun_exp(self): + _uuid = str(uuid.uuid4()) + ExecutionRequest.objects.create(exec_id=_uuid, func_name="test") + try: + _exec = self.orchestrator.get_execution_object(_uuid) + self.assertIsNotNone(_exec) + finally: + ExecutionRequest.objects.filter(exec_id=_uuid).delete() + + def test_create_execution_request(self): + handler = self.orchestrator.load_handler("geonode.upload.handlers.gpkg.handler.GPKGFileHandler") + count = ExecutionRequest.objects.count() + input_files = { + "files": {"base_file": "/tmp/file.txt"}, + "store_spatial_files": True, + } + exec_id = self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name=next(iter(handler.get_task_list(action="import"))), + step=next(iter(handler.get_task_list(action="import"))), + input_params={ + "files": {"base_file": "/tmp/file.txt"}, + "store_spatial_files": True, + }, + ) + exec_obj = ExecutionRequest.objects.filter(exec_id=exec_id).first() + self.assertEqual(count + 1, ExecutionRequest.objects.count()) + self.assertDictEqual(input_files, exec_obj.input_params) + self.assertEqual(exec_obj.STATUS_READY, exec_obj.status) + + @patch("geonode.upload.orchestrator.importer_app.tasks.get") + def test_perform_next_step(self, mock_celery): + # setup test + handler = self.orchestrator.load_handler("geonode.upload.handlers.gpkg.handler.GPKGFileHandler") + _id = self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name=next(iter(handler.get_task_list(action="import"))), + step="start_import", # adding the first step for the GPKG file + input_params={ + "files": {"base_file": "/tmp/file.txt"}, + "store_spatial_files": True, + }, + ) + # test under tests + self.orchestrator.perform_next_step( + _id, + "import", + step="start_import", + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + mock_celery.assert_called_once() + mock_celery.assert_called_with("geonode.upload.import_resource") + + @override_settings(MEDIA_ROOT="/tmp/") + @patch("geonode.upload.orchestrator.importer_app.tasks.get") + def test_perform_last_import_step(self, mock_celery): + # setup test + handler = self.orchestrator.load_handler("geonode.upload.handlers.gpkg.handler.GPKGFileHandler") + _id = self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name=next(iter(handler.get_task_list(action="import"))), + step="geonode.upload.create_geonode_resource", # adding the first step for the GPKG file + input_params={ + "files": {"base_file": "/tmp/file.txt"}, + "store_spatial_files": True, + }, + ) + # test under tests + self.orchestrator.perform_next_step( + _id, + "import", + step="geonode.upload.create_geonode_resource", + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + mock_celery.assert_not_called() + + @patch("geonode.upload.orchestrator.importer_app.tasks.get") + def test_perform_with_error_set_invalid_status(self, mock_celery): + mock_celery.side_effect = Exception("test exception") + # setup test + handler = self.orchestrator.load_handler("geonode.upload.handlers.gpkg.handler.GPKGFileHandler") + _id = self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name=next(iter(handler.get_task_list(action="import"))), + step="start_import", # adding the first step for the GPKG file + input_params={ + "files": {"base_file": "/tmp/file.txt"}, + "store_spatial_files": True, + }, + ) + # test under tests + with self.assertRaises(Exception): + self.orchestrator.perform_next_step( + _id, + "import", + step="start_import", + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + + _excec = ExecutionRequest.objects.filter(exec_id=_id).first() + self.assertIsNotNone(_excec) + self.assertEqual(ExecutionRequest.STATUS_FAILED, _excec.status) + + def test_set_as_failed(self): + # creating the temporary file that will be deleted + os.makedirs(settings.ASSETS_ROOT, exist_ok=True) + + fake_path = f"{settings.ASSETS_ROOT}to_be_deleted_file.txt" + with open(fake_path, "w"): + pass + + user = get_user_model().objects.first() + asset_handler = asset_handler_registry.get_default_handler() + + asset = asset_handler.create( + title="Original", + owner=user, + description=None, + type="gpkg", + files=[fake_path], + clone_files=False, + ) + + self.assertTrue(os.path.exists(fake_path)) + # we need to create first the execution + _uuid = self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="name", + step="geonode.upload.create_geonode_resource", # adding the first step for the GPKG file + input_params={ + "files": {"base_file": fake_path}, + "store_spatial_files": True, + "asset_id": asset.id, + "asset_module_path": f"{asset.__module__}.{asset.__class__.__name__}", + }, + ) + _uuid = str(_uuid) + self.orchestrator.set_as_failed(_uuid, reason="automatic test") + + # check normal execution status + req = ExecutionRequest.objects.get(exec_id=_uuid) + self.assertTrue(req.status, ExecutionRequest.STATUS_FAILED) + self.assertTrue(req.log, "automatic test") + # cleanup + req.delete() + + def test_set_as_completed(self): + # we need to create first the execution + _uuid = self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="name", + step="geonode.upload.create_geonode_resource", # adding the first step for the GPKG file + input_params={ + "files": {"base_file": "/tmp/file.txt"}, + "store_spatial_files": True, + }, + ) + + # calling the function + self.orchestrator.set_as_completed(_uuid) + + req = ExecutionRequest.objects.get(exec_id=_uuid) + self.assertTrue(req.status, ExecutionRequest.STATUS_FINISHED) + + # cleanup + req.delete() + + def test_update_execution_request_status(self): + # we need to create first the execution + _uuid = self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="name", + step="geonode.upload.create_geonode_resource", # adding the first step for the GPKG file + input_params={ + "files": {"base_file": "/tmp/file.txt"}, + "store_spatial_files": True, + }, + ) + + self.orchestrator.update_execution_request_status( + execution_id=_uuid, + status=ExecutionRequest.STATUS_RUNNING, + last_updated=timezone.now(), + func_name="function_name", + step="step_here", + ) + req = ExecutionRequest.objects.get(exec_id=_uuid) + self.assertTrue(req.status, ExecutionRequest.STATUS_RUNNING) + self.assertTrue(req.func_name, "function_name") + self.assertTrue(req.step, "step_here") + + # cleanup + req.delete() + + def test_evaluate_execution_progress_should_continue_if_some_task_is_not_finished( + self, + ): + # create the celery task result entry + try: + exec_id = str( + self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="test", + step="test", + ) + ) + + started_entry = TaskResult.objects.create(task_id="task_id_started", status="STARTED", task_args=exec_id) + success_entry = TaskResult.objects.create(task_id="task_id_success", status="SUCCESS", task_args=exec_id) + with self.assertLogs(level="INFO") as _log: + result = self.orchestrator.evaluate_execution_progress(exec_id) + + self.assertIsNone(result) + self.assertEqual( + f"INFO:importer:Execution with ID {exec_id} is completed. All tasks are done", + _log.output[0], + ) + + finally: + if started_entry: + started_entry.delete() + if success_entry: + success_entry.delete() + + def test_evaluate_execution_progress_should_fail_if_one_task_is_failed(self): + """ + Should set it fail if all the execution are done and at least 1 is failed + """ + # create the celery task result entry + os.makedirs(settings.ASSETS_ROOT, exist_ok=True) + + fake_path = f"{settings.ASSETS_ROOT}/file.txt" + with open(fake_path, "w"): + pass + + user = get_user_model().objects.first() + asset_handler = asset_handler_registry.get_default_handler() + + asset = asset_handler.create( + title="Original", + owner=user, + description=None, + type="gpkg", + files=[fake_path], + clone_files=False, + ) + + try: + exec_id = str( + self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="test", + step="test", + input_params={ + "asset_id": asset.id, + "asset_module_path": f"{asset.__module__}.{asset.__class__.__name__}", + }, + ) + ) + + FAILED_entry = TaskResult.objects.create(task_id="task_id_FAILED", status="FAILURE", task_args=exec_id) + success_entry = TaskResult.objects.create(task_id="task_id_success", status="SUCCESS", task_args=exec_id) + self.orchestrator.evaluate_execution_progress(exec_id) + + finally: + if FAILED_entry: + FAILED_entry.delete() + if success_entry: + success_entry.delete() + + def test_evaluate_execution_progress_should_set_as_completed(self): + try: + exec_id = str( + self.orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="test", + step="test", + ) + ) + + success_entry = TaskResult.objects.create(task_id="task_id_success", status="SUCCESS", task_args=exec_id) + + self.orchestrator.evaluate_execution_progress(exec_id) + + finally: + if success_entry: + success_entry.delete() diff --git a/geonode/upload/tests/unit/test_publisher.py b/geonode/upload/tests/unit/test_publisher.py new file mode 100644 index 00000000000..0117d0a5760 --- /dev/null +++ b/geonode/upload/tests/unit/test_publisher.py @@ -0,0 +1,104 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +from django.test import TestCase +from mock import patch +from geonode.upload import project_dir +from geonode.upload.publisher import DataPublisher +from unittest.mock import MagicMock + + +class TestDataPublisher(TestCase): + """ + Test to get the information and publish the resource in geoserver + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.publisher = DataPublisher(handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler") + cls.gpkg_path = f"{project_dir}/tests/fixture/valid.gpkg" + + def setUp(self): + layer = self.publisher.cat.get_resources("stazioni_metropolitana", workspaces="geonode") + print("delete layer") + if layer: + res = self.publisher.cat.delete(layer.resource, purge="all", recurse=True) + print(res.status_code) + print(res.json) + + def tearDown(self): + layer = self.publisher.cat.get_resources("stazioni_metropolitana", workspaces="geonode") + print("delete layer teardown") + if layer: + self.publisher.cat.delete(layer) + + res = self.publisher.cat.delete(layer.resource, purge="all", recurse=True) + print(res.status_code) + print(res.json) + + def test_extract_resource_name_and_crs(self): + """ + Given a layer and the original file, should extract the crs and the name + to let it publish in Geoserver + """ + values_found = self.publisher.extract_resource_to_publish( + files={"base_file": self.gpkg_path}, + action="import", + layer_name="stazioni_metropolitana", + ) + expected = {"crs": "EPSG:32632", "name": "stazioni_metropolitana"} + self.assertDictEqual(expected, values_found[0]) + + def test_extract_resource_name_and_crs_return_empty_if_the_file_does_not_exists( + self, + ): + """ + Given a layer and the original file, should extract the crs and the name + to let it publish in Geoserver + """ + values_found = self.publisher.extract_resource_to_publish( + files={"base_file": "/wrong/path/file.gpkg"}, + action="import", + layer_name="stazioni_metropolitana", + ) + self.assertListEqual([], values_found) + + @patch("geonode.upload.publisher.create_geoserver_db_featurestore") + def test_get_or_create_store_creation_should_called(self, datastore): + with patch.dict(os.environ, {"GEONODE_GEODATABASE": "not_existsing_db"}, clear=True): + self.publisher.get_or_create_store() + datastore.assert_called_once() + + @patch("geonode.upload.publisher.Catalog.publish_featuretype") + def test_publish_resources_should_raise_exception_if_any_error_happen(self, publish_featuretype): + publish_featuretype.side_effect = Exception("Exception") + + with self.assertRaises(Exception): + self.publisher.publish_resources(resources=[{"crs": "EPSG:32632", "name": "stazioni_metropolitana"}]) + publish_featuretype.assert_called_once() + + @patch("geonode.upload.publisher.Catalog.publish_featuretype") + def test_publish_resources_should_work(self, publish_featuretype): + publish_featuretype.return_value = True + self.publisher.sanity_checks = MagicMock() + result = self.publisher.publish_resources(resources=[{"crs": "EPSG:32632", "name": "stazioni_metropolitana"}]) + + self.assertTrue(result) + publish_featuretype.assert_called_once() diff --git a/geonode/upload/tests/unit/test_task.py b/geonode/upload/tests/unit/test_task.py new file mode 100644 index 00000000000..1f374da4112 --- /dev/null +++ b/geonode/upload/tests/unit/test_task.py @@ -0,0 +1,667 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +import shutil + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test.utils import override_settings +from unittest.mock import patch +from geonode.upload.api.exceptions import InvalidInputFileException + +from geonode.upload.celery_tasks import ( + copy_dynamic_model, + copy_geonode_data_table, + copy_geonode_resource, + create_dynamic_structure, + create_geonode_resource, + import_orchestrator, + import_resource, + orchestrator, + publish_resource, + rollback, +) +from geonode.resource.models import ExecutionRequest +from geonode.layers.models import Dataset +from geonode.resource.enumerator import ExecutionRequestAction +from geonode.base.models import ResourceBase +from geonode.base.populate_test_data import create_single_dataset +from geonode.assets.handlers import asset_handler_registry +from dynamic_models.models import ModelSchema, FieldSchema +from dynamic_models.exceptions import DynamicModelError, InvalidFieldNameError +from geonode.upload.models import ResourceHandlerInfo +from geonode.upload import project_dir + +from geonode.upload.tests.utils import ( + ImporterBaseTestSupport, + TransactionImporterBaseTestSupport, +) + +# Create your tests here. + + +class TestCeleryTasks(ImporterBaseTestSupport): + def setUp(self): + self.user = get_user_model().objects.first() + self.existing_file = f"{project_dir}/tests/fixture/valid.gpkg" + self.asset_handler = asset_handler_registry.get_default_handler() + + self.asset = self.asset_handler.create( + title="Original", + owner=self.user, + description=None, + type="gpkg", + files=[self.existing_file], + clone_files=False, + ) + + self.exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.get(username=self.user), + func_name="dummy_func", + step="dummy_step", + input_params={ + "files": {"base_file": self.existing_file}, + # "overwrite_existing_layer": True, + "store_spatial_files": True, + "asset_id": self.asset.id, + "asset_module_path": f"{self.asset.__module__}.{self.asset.__class__.__name__}", + }, + ) + + @patch("geonode.upload.celery_tasks.orchestrator.perform_next_step") + def test_import_orchestrator_dont_create_exececution_request_if_not__none(self, importer): + user = get_user_model().objects.first() + count = ExecutionRequest.objects.count() + + import_orchestrator( + files={"base_file": "/tmp/file.gpkg"}, + store_spatial_files=True, + user=user.username, + execution_id="some value", + ) + + self.assertEqual(count, ExecutionRequest.objects.count()) + importer.assert_called_once() + + @patch("geonode.upload.celery_tasks.orchestrator.perform_next_step") + @patch("geonode.upload.celery_tasks.DataStoreManager.input_is_valid") + def test_import_resource_should_rase_exp_if_is_invalid( + self, + is_valid, + importer, + ): + user = get_user_model().objects.first() + + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.get(username=user), + func_name="dummy_func", + step="dummy_step", + input_params={"files": self.existing_file, "store_spatial_files": True}, + ) + + is_valid.side_effect = Exception("Invalid format type") + + with self.assertRaises(InvalidInputFileException) as _exc: + import_resource( + str(exec_id), + action=ExecutionRequestAction.IMPORT.value, + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + expected_msg = f"Invalid format type. Request: {str(exec_id)}" + self.assertEqual(expected_msg, str(_exc.exception.detail)) + ExecutionRequest.objects.filter(exec_id=str(exec_id)).delete() + + @patch("geonode.upload.celery_tasks.orchestrator.perform_next_step") + @patch("geonode.upload.celery_tasks.DataStoreManager.input_is_valid") + @patch("geonode.upload.celery_tasks.DataStoreManager.prepare_import") + @patch("geonode.upload.celery_tasks.DataStoreManager.start_import") + def test_import_resource_should_work( + self, + prepare_import, + start_import, + is_valid, + importer, + ): + is_valid.return_value = True + user = get_user_model().objects.first() + + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.get(username=user), + func_name="dummy_func", + step="dummy_step", + input_params={"files": self.existing_file, "store_spatial_files": True}, + ) + + import_resource( + str(exec_id), + resource_type="gpkg", + action=ExecutionRequestAction.IMPORT.value, + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + + prepare_import.assert_called_once() + start_import.assert_called_once() + ExecutionRequest.objects.filter(exec_id=str(exec_id)).delete() + + @patch("geonode.upload.celery_tasks.import_orchestrator.apply_async") + @patch("geonode.upload.celery_tasks.DataPublisher.extract_resource_to_publish") + @patch("geonode.upload.celery_tasks.DataPublisher.publish_resources") + def test_publish_resource_should_work( + self, + publish_resources, + extract_resource_to_publish, + importer, + ): + try: + publish_resources.return_value = True + extract_resource_to_publish.return_value = [{"crs": 12345, "name": "dataset3"}] + + publish_resource( + str(self.exec_id), + resource_type="gpkg", + step_name="publish_resource", + layer_name="dataset3", + alternate="alternate_dataset3", + action=ExecutionRequestAction.IMPORT.value, + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + + # Evaluation + req = ExecutionRequest.objects.get(exec_id=str(self.exec_id)) + self.assertEqual(publish_resources.call_count, 1) + self.assertEqual("geonode.upload.publish_resource", req.step) + importer.assert_called_once() + finally: + # cleanup + if self.exec_id: + ExecutionRequest.objects.filter(exec_id=str(self.exec_id)).delete() + + @patch("geonode.upload.celery_tasks.import_orchestrator.apply_async") + @patch("geonode.upload.celery_tasks.DataPublisher.extract_resource_to_publish") + @patch("geonode.upload.celery_tasks.DataPublisher.publish_resources") + def test_publish_resource_if_overwrite_should_call_the_publishing( + self, + publish_resources, + extract_resource_to_publish, + importer, + ): + """ + Publish resource should be called since the resource does not exists in geoserver + even if an overwrite is required + """ + try: + publish_resources.return_value = True + extract_resource_to_publish.return_value = [{"crs": 12345, "name": "dataset3"}] + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.get(username=self.user), + func_name="dummy_func", + step="dummy_step", + input_params={ + "files": {"base_file": self.existing_file}, + "overwrite_existing_layer": True, + "store_spatial_files": True, + }, + ) + publish_resource( + str(exec_id), + resource_type="gpkg", + step_name="publish_resource", + layer_name="dataset3", + alternate="alternate_dataset3", + action=ExecutionRequestAction.IMPORT.value, + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + + # Evaluation + req = ExecutionRequest.objects.get(exec_id=str(exec_id)) + self.assertEqual("geonode.upload.publish_resource", req.step) + publish_resources.assert_called_once() + importer.assert_called_once() + + finally: + # cleanup + if exec_id: + ExecutionRequest.objects.filter(exec_id=str(exec_id)).delete() + + @patch("geonode.upload.celery_tasks.import_orchestrator.apply_async") + @patch("geonode.upload.celery_tasks.DataPublisher.extract_resource_to_publish") + @patch("geonode.upload.celery_tasks.DataPublisher.publish_resources") + @patch("geonode.upload.celery_tasks.DataPublisher.get_resource") + def test_publish_resource_if_overwrite_should_not_call_the_publishing( + self, + get_resource, + publish_resources, + extract_resource_to_publish, + importer, + ): + """ + Publish resource should be called since the resource does not exists in geoserver + even if an overwrite is required. + Should raise error if the crs is not found + """ + try: + with self.assertRaises(Exception): + get_resource.return_value = True + publish_resources.return_value = True + extract_resource_to_publish.return_value = [{"crs": 4326, "name": "dataset3"}] + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.get(username=self.user), + func_name="dummy_func", + step="dummy_step", + input_params={ + "files": {"base_file": self.existing_file}, + "overwrite_existing_layer": True, + "store_spatial_files": True, + }, + ) + publish_resource( + str(exec_id), + resource_type="gpkg", + step_name="publish_resource", + layer_name="dataset3", + alternate="alternate_dataset3", + action=ExecutionRequestAction.IMPORT.value, + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + ) + + # Evaluation + req = ExecutionRequest.objects.get(exec_id=str(exec_id)) + self.assertEqual("geonode.upload.publish_resource", req.step) + publish_resources.assert_not_called() + importer.assert_called_once() + + finally: + # cleanup + if exec_id: + ExecutionRequest.objects.filter(exec_id=str(exec_id)).delete() + + @patch("geonode.upload.celery_tasks.import_orchestrator.apply_async") + def test_create_geonode_resource(self, import_orchestrator): + try: + alternate = "geonode:alternate_foo_dataset" + self.assertFalse(Dataset.objects.filter(alternate=alternate).exists()) + + create_geonode_resource( + str(self.exec_id), + resource_type="gpkg", + step_name="create_geonode_resource", + layer_name="foo_dataset", + alternate="alternate_foo_dataset", + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + action="import", + ) + + # Evaluation + req = ExecutionRequest.objects.get(exec_id=str(self.exec_id)) + self.assertEqual("geonode.upload.create_geonode_resource", req.step) + + self.assertTrue(Dataset.objects.filter(alternate=alternate).exists()) + + import_orchestrator.assert_called_once() + + finally: + # cleanup + if Dataset.objects.filter(alternate=alternate).exists(): + Dataset.objects.filter(alternate=alternate).delete() + + @patch("geonode.upload.celery_tasks.call_rollback_function") + def test_copy_geonode_resource_should_raise_exeption_if_the_alternate_not_exists(self, call_rollback_function): + with self.assertRaises(Exception): + copy_geonode_resource( + str(self.exec_id), + "geonode.upload.copy_geonode_resource", + "cloning", + "invalid_alternate", + "geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + "copy", + kwargs={ + "original_dataset_alternate": "geonode:example_dataset", + "new_dataset_alternate": "geonode:schema_copy_example_dataset", # this alternate is generated dring the geonode resource copy + }, + ) + call_rollback_function.assert_called_once() + + @patch("geonode.upload.celery_tasks.import_orchestrator.apply_async") + def test_copy_geonode_resource(self, async_call): + alternate = "geonode:cloning" + new_alternate = None + try: + rasource = create_single_dataset(name="cloning") + + exec_id, new_alternate = copy_geonode_resource( + str(self.exec_id), + "geonode.upload.copy_geonode_resource", + "cloning", + rasource.alternate, + "geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + "copy", + kwargs={ + "original_dataset_alternate": "geonode:cloning", + "new_dataset_alternate": "geonode:schema_copy_cloning", # this alternate is generated dring the geonode resource copy + }, + ) + + self.assertTrue(ResourceBase.objects.filter(alternate__icontains=new_alternate).exists()) + async_call.assert_called_once() + + finally: + # cleanup + if Dataset.objects.filter(alternate=alternate).exists(): + Dataset.objects.filter(alternate=alternate).delete() + if new_alternate: + Dataset.objects.filter(alternate=new_alternate).delete() + + @patch("geonode.upload.handlers.gpkg.handler.GPKGFileHandler._import_resource_rollback") + @patch("geonode.upload.handlers.gpkg.handler.GPKGFileHandler._publish_resource_rollback") + @patch("geonode.upload.handlers.gpkg.handler.GPKGFileHandler._create_geonode_resource_rollback") + @override_settings(MEDIA_ROOT="/tmp/") + def test_rollback_works_as_expected_vector_step( + self, + _create_geonode_resource_rollback, + _publish_resource_rollback, + _import_resource_rollback, + ): + """ + rollback should remove the resource based on the step it has reached + """ + test_config = [ + ("geonode.upload.import_resource", [_import_resource_rollback]), + ( + "geonode.upload.publish_resource", + [_import_resource_rollback, _publish_resource_rollback], + ), + ( + "geonode.upload.create_geonode_resource", + [ + _import_resource_rollback, + _publish_resource_rollback, + _create_geonode_resource_rollback, + ], + ), + ] + for conf in test_config: + try: + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.get(username=self.user), + func_name="dummy_func", + step=conf[0], # step name + action="import", + input_params={ + "files": {"base_file": self.existing_file}, + "store_spatial_files": True, + "handler_module_path": "geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + }, + ) + rollback(str(exec_id)) + + # Evaluation + req = ExecutionRequest.objects.get(exec_id=str(exec_id)) + self.assertEqual("geonode.upload.rollback", req.step) + self.assertTrue(req.status == "failed") + for expected_function in conf[1]: + expected_function.assert_called_once() + expected_function.reset_mock() + finally: + # cleanup + if exec_id: + ExecutionRequest.objects.filter(exec_id=str(exec_id)).delete() + + @patch("geonode.upload.handlers.geotiff.handler.GeoTiffFileHandler._import_resource_rollback") + @patch("geonode.upload.handlers.geotiff.handler.GeoTiffFileHandler._publish_resource_rollback") + @patch("geonode.upload.handlers.geotiff.handler.GeoTiffFileHandler._create_geonode_resource_rollback") + @override_settings(MEDIA_ROOT="/tmp/") + def test_rollback_works_as_expected_raster( + self, + _create_geonode_resource_rollback, + _publish_resource_rollback, + _import_resource_rollback, + ): + """ + rollback should remove the resource based on the step it has reached + """ + test_config = [ + ("geonode.upload.import_resource", [_import_resource_rollback]), + ( + "geonode.upload.publish_resource", + [_import_resource_rollback, _publish_resource_rollback], + ), + ( + "geonode.upload.create_geonode_resource", + [ + _import_resource_rollback, + _publish_resource_rollback, + _create_geonode_resource_rollback, + ], + ), + ] + for conf in test_config: + try: + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.get(username=self.user), + func_name="dummy_func", + step=conf[0], # step name + action="import", + input_params={ + "files": {"base_file": "/tmp/filepath"}, + "store_spatial_files": True, + "handler_module_path": "geonode.upload.handlers.geotiff.handler.GeoTiffFileHandler", + }, + ) + rollback(str(exec_id)) + + # Evaluation + req = ExecutionRequest.objects.get(exec_id=str(exec_id)) + self.assertEqual("geonode.upload.rollback", req.step) + self.assertTrue(req.status == "failed") + for expected_function in conf[1]: + expected_function.assert_called_once() + expected_function.reset_mock() + finally: + # cleanup + if exec_id: + ExecutionRequest.objects.filter(exec_id=str(exec_id)).delete() + + @override_settings(MEDIA_ROOT="/tmp/") + def test_import_metadata_should_work_as_expected(self): + handler = "geonode.upload.handlers.xml.handler.XMLFileHandler" + # lets copy the file to the temporary folder + # later will be removed + valid_xml = f"{settings.PROJECT_ROOT}/base/fixtures/test_xml.xml" + shutil.copy(valid_xml, "/tmp") + xml_in_tmp = "/tmp/test_xml.xml" + + user, _ = get_user_model().objects.get_or_create(username="admin") + valid_files = {"base_file": xml_in_tmp, "xml_file": xml_in_tmp} + + layer = create_single_dataset("test_dataset_importer") + exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.first(), + func_name="funct1", + step="step", + input_params={ + "files": valid_files, + "resource_pk": layer.pk, + "skip_existing_layer": True, + "handler_module_path": str(handler), + }, + ) + ResourceHandlerInfo.objects.create( + resource=layer, + handler_module_path="geonode.upload.handlers.shapefile.handler.ShapeFileHandler", + ) + + import_resource(str(exec_id), handler, "import") + + layer.refresh_from_db() + self.assertEqual(layer.title, "test_dataset") + + +class TestDynamicModelSchema(TransactionImporterBaseTestSupport): + databases = ("default", "datastore") + + def setUp(self): + self.user = get_user_model().objects.first() + self.existing_file = f"{project_dir}/tests/fixture/valid.gpkg" + self.exec_id = orchestrator.create_execution_request( + user=get_user_model().objects.get(username=self.user), + func_name="dummy_func", + step="dummy_step", + input_params={ + "files": {"base_file": self.existing_file}, + # "overwrite_existing_layer": True, + "store_spatial_files": True, + }, + ) + + def test_create_dynamic_structure_should_raise_error_if_schema_is_not_available( + self, + ): + with self.assertRaises(DynamicModelError) as _exc: + create_dynamic_structure( + execution_id=str(self.exec_id), + fields=[], + dynamic_model_schema_id=0, + overwrite=False, + layer_name="test_layer", + ) + + expected_msg = "The model with id 0 does not exists." + self.assertEqual(expected_msg, str(_exc.exception)) + + def test_create_dynamic_structure_should_raise_error_if_field_class_is_none(self): + try: + name = str(self.exec_id) + + schema = ModelSchema.objects.create(name=f"schema_{name}", db_name="datastore") + dynamic_fields = [ + {"name": "field1", "class_name": None, "null": True}, + ] + with self.assertRaises(InvalidFieldNameError) as _exc: + create_dynamic_structure( + execution_id=str(self.exec_id), + fields=dynamic_fields, + dynamic_model_schema_id=schema.pk, + overwrite=False, + layer_name="test_layer", + ) + + expected_msg = ( + "Error during the field creation. The field or class_name is None {'name': 'field1', 'class_name': None, 'null': True} for test_layer " + + f"for execution {name}" + ) + self.assertEqual(expected_msg, str(_exc.exception)) + finally: + ModelSchema.objects.filter(name=f"schema_{name}").delete() + + def test_create_dynamic_structure_should_work(self): + try: + name = str(self.exec_id) + + schema = ModelSchema.objects.create(name=f"schema_{name}", db_name="datastore") + dynamic_fields = [ + { + "name": "field1", + "class_name": "django.contrib.gis.db.models.fields.LineStringField", + "null": True, + }, + ] + + create_dynamic_structure( + execution_id=str(self.exec_id), + fields=dynamic_fields, + dynamic_model_schema_id=schema.pk, + overwrite=False, + layer_name="test_layer", + ) + + self.assertTrue(FieldSchema.objects.filter(name="field1").exists()) + + finally: + ModelSchema.objects.filter(name=f"schema_{name}").delete() + FieldSchema.objects.filter(name="field1").delete() + + @patch("geonode.upload.celery_tasks.import_orchestrator.apply_async") + @patch.dict(os.environ, {"IMPORTER_ENABLE_DYN_MODELS": "True"}) + def test_copy_dynamic_model_should_work(self, async_call): + try: + name = str(self.exec_id) + # setup model schema to be copied + schema = ModelSchema.objects.create( + name=f"schema_{name}", + db_name="datastore", + db_table_name=f"schema_{name}", + ) + FieldSchema.objects.create( + name=f"field_{name}", + class_name="django.contrib.gis.db.models.fields.LineStringField", + model_schema=schema, + ) + + layer = create_single_dataset(f"schema_{name}") + layer.alternate = f"geonode:schema_{name}" + layer.save() + + self.assertTrue(ModelSchema.objects.filter(name__icontains="schema_").count() == 1) + + copy_dynamic_model( + exec_id=str(self.exec_id), + actual_step="copy", + layer_name=f"schema_{name}", + alternate=f"geonode:schema_{name}", + handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + action=ExecutionRequestAction.COPY.value, + kwargs={ + "original_dataset_alternate": f"geonode:schema_{name}", + }, + ) + # the alternate is generated internally + self.assertTrue(ModelSchema.objects.filter(name=f"schema_{name}").exists()) + self.assertTrue(ModelSchema.objects.filter(name__icontains="schema_").count() == 2) + + schema = ModelSchema.objects.all() + for val in schema: + self.assertEqual(val.name, val.db_table_name) + + async_call.assert_called_once() + + finally: + ModelSchema.objects.filter(name="schema_").delete() + FieldSchema.objects.filter(name="field_").delete() + + @patch("geonode.upload.celery_tasks.import_orchestrator.apply_async") + @patch("geonode.upload.celery_tasks.connections") + def test_copy_geonode_data_table_should_work(self, mock_connection, async_call): + mock_cursor = mock_connection.__getitem__("datastore").cursor.return_value.__enter__.return_value + ModelSchema.objects.create(name=f"schema_copy_{str(self.exec_id)}", db_name="datastore") + + copy_geonode_data_table( + exec_id=str(self.exec_id), + actual_step="copy", + layer_name=f"schema_{str(self.exec_id)}", + alternate=f"geonode:schema_{str(self.exec_id)}", + handlers_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", + action=ExecutionRequestAction.COPY.value, + kwargs={ + "original_dataset_alternate": f"geonode:schema_{str(self.exec_id)}", + "new_dataset_alternate": f"schema_copy_{str(self.exec_id)}", # this alternate is generated dring the geonode resource copy + }, + ) + mock_cursor.execute.assert_called_once() + mock_cursor.execute.assert_called() + async_call.assert_called_once() diff --git a/geonode/upload/tests/test_utils.py b/geonode/upload/tests/unit/test_utils.py similarity index 63% rename from geonode/upload/tests/test_utils.py rename to geonode/upload/tests/unit/test_utils.py index e3f5417972d..c3f342fcd7c 100644 --- a/geonode/upload/tests/test_utils.py +++ b/geonode/upload/tests/unit/test_utils.py @@ -22,47 +22,11 @@ from django.conf import settings from geonode.tests.base import GeoNodeBaseTestSupport -from lxml import etree - -from geonode.upload import utils from geonode.upload.models import UploadSizeLimit, UploadParallelismLimit from geonode.upload.utils import get_max_upload_size, get_max_upload_parallelism_limit class UtilsTestCase(GeoNodeBaseTestSupport): - def test_pages(self): - self.assertIn("kml-overlay", utils._pages) - - def test_get_kml_doc(self): - kml_bytes = """ - - - - CSR5r3_annual - - CSR5r3_annual - - ffffffff - 1 - 0 - - CSR5r3_annual.png - 1 - - - 70.000000 - -60.500000 - 180.000000 - -180.000000 - 0.0000000000000000 - - - - - """.strip() - kml_doc, ns = utils.get_kml_doc(kml_bytes) - self.assertTrue(etree.QName(kml_doc.tag).localname, "kml") - self.assertIn("kml", ns.keys()) def test_get_max_upload_size(self): upload_size = UploadSizeLimit.objects.create(slug="test_slug", max_size=1000, description="test description") diff --git a/geonode/upload/tests/utils.py b/geonode/upload/tests/utils.py index 9d687fe6c5f..e0e0165bfdd 100644 --- a/geonode/upload/tests/utils.py +++ b/geonode/upload/tests/utils.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2021 OSGeo +# Copyright (C) 2024 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 @@ -16,65 +16,53 @@ # along with this program. If not, see . # ######################################################################### +from django.core.management import call_command +from django.test import TestCase, TransactionTestCase -import os -import logging -from io import IOBase -from urllib.request import urljoin +class ImporterBaseTestSupport(TestCase): + databases = ("default", "datastore") + multi_db = True -from django.urls import reverse -from django.contrib.auth import authenticate + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + """ + Why manually load the fixture after the setupClass? + Django in the setUpClass method, load the fixture in all the databases + that are defined in the databases attribute. The problem is that the + datastore database will contain only the dyanmic models infrastructure + and not the whole geonode structure. So that, having the fixture as a + attribute will raise and error + """ + fixture = [ + "initial_data.json", + "group_test_data.json", + "default_oauth_apps.json", + ] -logger = logging.getLogger(__name__) + call_command("loaddata", *fixture, **{"verbosity": 0, "database": "default"}) -GEONODE_USER = "admin" -GEONODE_PASSWD = "admin" +class TransactionImporterBaseTestSupport(TransactionTestCase): + databases = ("default", "datastore") + multi_db = True -def rest_upload_by_path(_file, client, username=GEONODE_USER, password=GEONODE_PASSWD, non_interactive=False): - """function that uploads a file, or a collection of files, to - the GeoNode""" - assert authenticate(username=username, password=password) - client.login(username=username, password=password) - spatial_files = ("dbf_file", "shx_file", "prj_file") - base, ext = os.path.splitext(_file) - params = { - # make public since wms client doesn't do authentication - "permissions": '{ "users": {"AnonymousUser": ["view_resourcebase"]} , "groups":{}}', - "time": "false", - "charset": "UTF-8", - } + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + """ + Why manually load the fixture after the setupClass? + Django in the setUpClass method, load the fixture in all the databases + that are defined in the databases attribute. The problem is that the + datastore database will contain only the dyanmic models infrastructure + and not the whole geonode structure. So that, having the fixture as a + attribute will raise and error + """ + fixture = [ + "initial_data.json", + "group_test_data.json", + "default_oauth_apps.json", + ] - # deal with shapefiles - if ext.lower() == ".shp": - for spatial_file in spatial_files: - ext, _ = spatial_file.split("_") - file_path = f"{base}.{ext}" - # sometimes a shapefile is missing an extra file, - # allow for that - if os.path.exists(file_path): - params[spatial_file] = open(file_path, "rb") - - with open(_file, "rb") as base_file: - params["base_file"] = base_file - for name, value in params.items(): - if isinstance(value, IOBase): - params[name] = value - url = urljoin(f"{reverse('uploads-list')}/", "upload/") - if non_interactive: - params["non_interactive"] = "true" - logger.error(f" ---- UPLOAD URL: {url}") - response = client.post(url, data=params) - - # Closes the files - for spatial_file in spatial_files: - if isinstance(params.get(spatial_file), IOBase): - params[spatial_file].close() - - try: - logger.error(f" -- response: {response.status_code} / {response.json()}") - return response, response.json() - except (ValueError, TypeError): - logger.exception(ValueError(f"probably not json, status {response.status_code} / {response.content}")) - return response, response.content + call_command("loaddata", *fixture, **{"verbosity": 0, "database": "default"}) diff --git a/geonode/upload/upload.py b/geonode/upload/upload.py deleted file mode 100644 index 4a3feb25d7b..00000000000 --- a/geonode/upload/upload.py +++ /dev/null @@ -1,116 +0,0 @@ -######################################################################### -# -# 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 . -# -######################################################################### - -""" -Provide views and business logic of doing an upload. - -The upload process may be multi step so views are all handled internally here -by the view function. - -The pattern to support separation of view/logic is each step in the upload -process is suffixed with "_step". The view for that step is suffixed with -"_step_view". The goal of separation of view/logic is to support various -programmatic uses of this API. The logic steps should not accept request objects -or return response objects. - -State is stored in a UploaderSession object stored in the user's session. -This needs to be made more stateful by adding a model. -""" -import logging - - -logger = logging.getLogger(__name__) - - -class UploaderSession: - """All objects held must be able to survive a good pickling""" - - # the gsimporter session object - import_session = None - - # if provided, this file will be uploaded to geoserver and set as - # the default - import_sld_file = None - - # location of any temporary uploaded files - tempdir = None - - # the main uploaded file, zip, shp, tif, etc. - base_file = None - - # the name to try to give the layer - name = None - - # the input file charset - charset = "UTF-8" - - # blob of permissions JSON - permissions = None - - # store most recently configured time transforms to support deleting - time_transforms = None - - # defaults to REPLACE if not provided. Accepts APPEND, too - update_mode = None - - # Configure Time for this Dataset - time = None - - # the title given to the layer - dataset_title = None - - # the abstract - dataset_abstract = None - - # track the most recently completed upload step - completed_step = None - - # track the most recently completed upload step - error_msg = None - - # the upload type - see the _pages dict in views - upload_type = None - - # whether the files have been uploaded or provided locally - spatial_files_uploaded = True - - # time related info - need to store here until geoserver layer exists - time_info = None - - # whether the user has selected a time dimension for ImageMosaic granules - # or not - mosaic = None - append_to_mosaic_opts = None - append_to_mosaic_name = None - mosaic_time_regex = None - mosaic_time_value = None - - # the user who started this upload session - user = None - - def __init__(self, **kw): - for k, v in kw.items(): - if hasattr(self, k): - setattr(self, k, v) - else: - raise Exception(f"not handled : {k}") - - def cleanup(self): - """do what we should at the given state of the upload""" - pass diff --git a/geonode/upload/upload_validators.py b/geonode/upload/upload_validators.py deleted file mode 100644 index e194b6f2d88..00000000000 --- a/geonode/upload/upload_validators.py +++ /dev/null @@ -1,237 +0,0 @@ -######################################################################### -# -# Copyright (C) 2018 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 . -# -######################################################################### - -"""Tools for performing validation of uploaded spatial files.""" -import re -import os.path -import logging -import zipfile - -from collections import namedtuple - -from django import forms -from django.utils.translation import gettext_lazy as _ - -from . import files -from .utils import get_kml_doc - -logger = logging.getLogger(__name__) - -ShapefileAux = namedtuple("ShapefileAux", ["extension", "mandatory"]) - - -def _supported_type(ext, supported_types): - return any([type_.matches(ext) for type_ in supported_types]) - - -def _validate_shapefile_components(possible_filenames): - """Validates that a shapefile can be loaded from the input file paths - - :arg possible_files: Remaining form upload contents - :type possible_files: list - :raises: forms.ValidationError - - """ - - shp_files = [str(f) for f in possible_filenames if str(f).lower().endswith(".shp")] - aux_mandatory = True - if len(shp_files) > 1: - raise forms.ValidationError(_("Only one shapefile per zip is allowed")) - elif len(shp_files) == 0: - shp_files = [ - f for f in possible_filenames if os.path.splitext(f.lower())[1] in (".shp", ".dbf", ".shx", ".prj") - ] - aux_mandatory = False - try: - shape_component = shp_files[0] - except IndexError: - return None - base_name, base_extension = os.path.splitext(os.path.basename(shape_component)) - components = [base_extension[1:]] - shapefile_additional = [ - ShapefileAux(extension="dbf", mandatory=aux_mandatory), - ShapefileAux(extension="shx", mandatory=aux_mandatory), - ShapefileAux(extension="prj", mandatory=False), - ShapefileAux(extension="xml", mandatory=False), - ShapefileAux(extension="sld", mandatory=False), - ] - for additional_component in shapefile_additional: - for path in possible_filenames: - additional_name = os.path.splitext(os.path.basename(path))[0] - matches_main_name = bool(re.match(base_name, additional_name, re.I)) - extension = os.path.splitext(path)[1][1:].lower() - found_component = extension == additional_component.extension - if found_component and matches_main_name: - components.append(additional_component.extension) - break - else: - if additional_component.mandatory: - raise forms.ValidationError( - f"Could not find {additional_component.extension} file, which is mandatory for " "shapefile uploads" - ) - logger.debug(f"shapefile components: {components}") - return components - - -def _validate_kml_bytes(kml_bytes, other_files): - result = None - kml_doc, namespaces = get_kml_doc(kml_bytes) - ground_overlays = kml_doc.xpath("//kml:GroundOverlay", namespaces=namespaces) - if len(ground_overlays) > 1: - raise forms.ValidationError(_("kml files with more than one GroundOverlay are not supported")) - elif len(ground_overlays) == 1: - try: - image_path = ground_overlays[0].xpath("kml:Icon/kml:href/text()", namespaces=namespaces)[0].strip() - except IndexError: - image_path = "" - logger.debug(f"image_path: {image_path}") - logger.debug(f"other_files: {other_files}") - if image_path not in other_files: - raise forms.ValidationError(_("Ground overlay image declared in kml file cannot be found")) - result = ("kml", "sld", os.path.splitext(image_path)[-1][1:]) - return result - - -def validate_kml(possible_files): - """Validate uploaded KML file and a possible image companion file - - KML files that specify vectorial data typers are uploaded standalone. - However, if the KML specifies a GroundOverlay type (raster) they are - uploaded together with a raster file. - - """ - kml_file = [f for f in possible_files if f.name.lower().endswith(".kml")][0] - others = [f.name for f in possible_files if not f.name.lower().endswith(".kml")] - - kml_file.seek(0) - kml_bytes = kml_file.read() - result = _validate_kml_bytes(kml_bytes, others) - if not result: - kml_doc, namespaces = get_kml_doc(kml_bytes) - if kml_doc and namespaces: - return ( - "kml", - "sld", - ) - return result - - -def validate_kml_zip(kmz_django_file): - kml_bytes = None - with zipfile.ZipFile(kmz_django_file, allowZip64=True) as zip_handler: - zip_contents = zip_handler.namelist() - kml_files = [i for i in zip_contents if i.lower().endswith(".kml")] - if not kml_files: - return None - if len(kml_files) > 1: - raise forms.ValidationError(_("Only one kml file per ZIP is allowed")) - kml_zip_path = kml_files[0] - kml_bytes = zip_handler.read(kml_zip_path) - kml_doc, namespaces = get_kml_doc(kml_bytes) - if kml_doc and namespaces: - return ("zip",) - return None - - -def validate_kmz(kmz_django_file): - with zipfile.ZipFile(kmz_django_file, allowZip64=True) as zip_handler: - zip_contents = zip_handler.namelist() - kml_files = [i for i in zip_contents if i.lower().endswith(".kml")] - if len(kml_files) > 1: - raise forms.ValidationError(_("Only one kml file per kmz is allowed")) - try: - kml_zip_path = kml_files[0] - kml_bytes = zip_handler.read(kml_zip_path) - except IndexError: - return None - other_filenames = [i for i in zip_contents if not i.lower().endswith(".kml")] - if _validate_kml_bytes(kml_bytes, other_filenames): - return ("kmz",) - else: - return None - - -def validate_shapefile(zip_django_file): - valid_extensions = None - with zipfile.ZipFile(zip_django_file, allowZip64=True) as zip_handler: - contents = zip_handler.namelist() - if _validate_shapefile_components(contents): - valid_extensions = ("zip",) - return valid_extensions - - -def validate_raster(contents, allow_multiple=False): - def dupes(_a): - return {x for x in _a if _a.count(x) > 1} - - valid_extensions = None - raster_types = [t for t in files.types if t.dataset_type == files.raster] - raster_exts = [f".{t.code}" for t in raster_types] - raster_aliases = [] - for alias in [aliases for aliases in [t.aliases for t in raster_types] if aliases]: - raster_aliases.extend([f".{a}" for a in alias]) - raster_exts.extend(raster_aliases) - - raster_files = [f for f in contents if os.path.splitext(str(f).lower())[1] in raster_exts] - other_files = [f for f in contents if os.path.splitext(str(f).lower())[1] not in raster_exts] - - all_extensions = [os.path.splitext(str(f))[1][1:] for f in raster_files] - other_extensions = tuple({os.path.splitext(str(f))[1][1:] for f in other_files}) - valid_extensions = tuple(set(all_extensions)) - dup_extensions = tuple(dupes(all_extensions)) - if dup_extensions: - geotiff_extensions = [x for x in dup_extensions if x in files._tif_extensions] - mosaics_extensions = [x for x in other_extensions if x in files._mosaics_extensions] - if mosaics_extensions: - return ("zip-mosaic",) - elif geotiff_extensions: - if not allow_multiple: - raise forms.ValidationError( - _("You are trying to upload multiple GeoTIFFs without a valid 'indexer.properties' file.") - ) - else: - return ("zip-mosaic",) - else: - raise forms.ValidationError(_("Only one raster file per ZIP is allowed")) - else: - if valid_extensions: - if len(valid_extensions) > 1 and not allow_multiple: - raise forms.ValidationError(_("No multiple rasters allowed")) - else: - if not allow_multiple or ( - "properties" not in other_extensions and ("sld" in other_extensions or "xml" in other_extensions) - ): - return valid_extensions + other_extensions - else: - return ("zip-mosaic",) - else: - return None - - -def validate_raster_zip(zip_django_file): - valid_extensions = None - with zipfile.ZipFile(zip_django_file, allowZip64=True) as zip_handler: - contents = zip_handler.namelist() - valid_extensions = validate_raster(contents, allow_multiple=True) - if valid_extensions: - if "zip-mosaic" not in valid_extensions: - return ("zip",) - else: - return ("zip-mosaic",) - return None diff --git a/geonode/upload/uploadhandler.py b/geonode/upload/uploadhandler.py index 59ad3ce562c..76c33c4f520 100644 --- a/geonode/upload/uploadhandler.py +++ b/geonode/upload/uploadhandler.py @@ -1,3 +1,21 @@ +######################################################################### +# +# Copyright (C) 2024 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 base64 import binascii import html diff --git a/geonode/upload/urls.py b/geonode/upload/urls.py index 1d11fbc40e5..7e72ecec55a 100644 --- a/geonode/upload/urls.py +++ b/geonode/upload/urls.py @@ -16,11 +16,10 @@ # along with this program. If not, see . # ######################################################################### -from django.urls import include, re_path +from geonode.api.urls import router +from geonode.upload.api import views -from . import views +urlpatterns = [] # 'geonode.upload.views', -urlpatterns = [ # 'geonode.upload.views', - re_path(r"^delete/(?P\d+)?$", views.delete, name="data_upload_delete"), - re_path(r"^", include("geonode.upload.api.urls")), -] +router.register(r"upload-size-limits", views.UploadSizeLimitViewSet, "upload-size-limits") +router.register(r"upload-parallelism-limits", views.UploadParallelismLimitViewSet, "upload-parallelism-limits") diff --git a/geonode/upload/utils.py b/geonode/upload/utils.py index da992b5c89c..a82147b6253 100644 --- a/geonode/upload/utils.py +++ b/geonode/upload/utils.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2024 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 @@ -16,376 +16,20 @@ # along with this program. If not, see . # ######################################################################### -import re -import os -import json -import logging -import zipfile -import traceback - -from osgeo import ogr -from lxml import etree -from itertools import islice -from owslib.etree import etree as dlxml - -from django.conf import settings -from django.urls import reverse -from django.core.exceptions import ObjectDoesNotExist +import enum +from geonode.resource.manager import ResourceManager +from geonode.geoserver.manager import GeoServerResourceManager +from geonode.base.models import ResourceBase from django.utils.translation import gettext_lazy as _ -from django.http import HttpResponse -from django.template.defaultfilters import filesizeformat - -from geoserver.catalog import FailedRequestError, ConflictingDataError - from geonode.upload.api.exceptions import ( FileUploadLimitException, - GeneralUploadException, UploadParallelismLimitException, ) from geonode.upload.models import UploadSizeLimit, UploadParallelismLimit -from geonode.utils import json_response as do_json_response, unzip_file, mkdtemp -from geonode.geoserver.helpers import ( - gs_catalog, - gs_uploader, - ogc_server_settings, - get_store, - set_time_dimension, -) # mosaic_delete_first_granule +from django.template.defaultfilters import filesizeformat from geonode.resource.models import ExecutionRequest - -ogr.UseExceptions() - -logger = logging.getLogger(__name__) - - -def _log(msg, *args, level="error"): - # this logger is used also for debug purpose with error level - getattr(logger, level)(msg, *args) - - -iso8601 = re.compile( - r"^(?P((?P\d{4})([/-]?(?P(0[1-9])|(1[012]))" - + r"([/-]?(?P(0[1-9])|([12]\d)|(3[01])))?)?(?:[ T]?(?P([01][0-9])" - + r"|(?:2[0123]))(\:?(?P[0-5][0-9])(\:?(?P[0-5][0-9]([\,\.]\d{1,10})?))?)" - + r"?(?:Z|([\-+](?:([01][0-9])|(?:2[0123]))(\:?(?:[0-5][0-9]))?))?)?))$" -).match - -_SUPPORTED_CRS = getattr(settings, "UPLOADER", None) -if _SUPPORTED_CRS: - _SUPPORTED_CRS = _SUPPORTED_CRS.get("SUPPORTED_CRS", ["EPSG:4326", "EPSG:3857"]) - -_SUPPORTED_EXT = getattr(settings, "UPLOADER", None) -if _SUPPORTED_EXT: - _SUPPORTED_EXT = _SUPPORTED_EXT.get( - "SUPPORTED_EXT", - [".shp", ".csv", ".kml", ".kmz", ".json", ".geojson", ".tif", ".tiff", ".geotiff", ".gml", ".xml"], - ) - -_ALLOW_TIME_STEP = getattr(settings, "UPLOADER", False) -if _ALLOW_TIME_STEP: - _ALLOW_TIME_STEP = _ALLOW_TIME_STEP.get("OPTIONS", False).get("TIME_ENABLED", False) - -_ALLOW_MOSAIC_STEP = getattr(settings, "UPLOADER", False) -if _ALLOW_MOSAIC_STEP: - _ALLOW_MOSAIC_STEP = _ALLOW_MOSAIC_STEP.get("OPTIONS", False).get("MOSAIC_ENABLED", False) - -_ASYNC_UPLOAD = ( - ogc_server_settings and ogc_server_settings.DATASTORE is not None and len(ogc_server_settings.DATASTORE) > 0 -) - -# at the moment, the various time support transformations require the database -if _ALLOW_TIME_STEP and not _ASYNC_UPLOAD: - raise Exception(_("To support the time step, you must enable the OGC_SERVER DATASTORE option")) - -_geoserver_down_error_msg = """ -GeoServer is not responding. Please try again later and sorry for the inconvenience. -""" - -_unexpected_error_msg = """ -An error occurred while trying to process your request. Our administrator has -been notified, but if you'd like, please note this error code -below and details on what you were doing when you encountered this error. -That information can help us identify the cause of the problem and help us with -fixing it. Thank you! -""" - - -class JSONResponse(HttpResponse): - """JSON response class.""" - - def __init__(self, obj="", json_opts=None, content_type="application/json", *args, **kwargs): - if json_opts is None: - json_opts = {} - content = json.dumps(obj, **json_opts) - super().__init__(content, content_type, *args, **kwargs) - - -def json_response(*args, **kw): - # if 'exception' in kw: - # logger.warn(traceback.format_exc(kw['exception'])) - return do_json_response(*args, **kw) - - -def json_load_byteified(file_handle): - return _byteify(json.load(file_handle, object_hook=_byteify), ignore_dicts=True) - - -def json_loads_byteified(json_text, charset): - return _byteify(json.loads(json_text, object_hook=_byteify), ignore_dicts=True) - - -def _byteify(data, ignore_dicts=False): - # if this is a unicode string, return its string representation - if isinstance(data, str): - return data - # if this is a list of values, return list of byteified values - if isinstance(data, list): - return [_byteify(item, ignore_dicts=True) for item in data] - # if this is a dictionary, return dictionary of byteified keys and values - # but only if we haven't already byteified it - if isinstance(data, dict) and not ignore_dicts: - return {_byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True) for key, value in data.items()} - # if it's anything else, return it in its original form - return data - - -def get_kml_doc(kml_bytes): - """Parse and return an etree element with the kml file's content""" - kml_doc = dlxml.fromstring(kml_bytes, parser=etree.XMLParser(resolve_entities=False)) - ns = kml_doc.nsmap.copy() - ns["kml"] = ns.pop(None) - return kml_doc, ns - - -""" - Upload Workflow: Steps Utilities -""" -# note 'run' is not a "real" step, but handled as a special case -# and 'save' is the implied first step :P -_pages = { - "shp": ("srs", "check", "time", "run", "final"), - "csv": ("csv", "srs", "check", "time", "run", "final"), - "tif": ("srs", "run", "final"), - "zip-mosaic": ("run", "final"), - "asc": ("run", "final"), - "kml": ("run", "final"), - "kml-overlay": ("run", "final"), - "geojson": ("run", "final"), - "ntf": ("run", "final"), # NITF - "img": ("run", "final"), # ERDAS Imagine - "i41": ("run", "final"), # CIB01 RPF - "i21": ("run", "final"), # CIB05 RPF - "i11": ("run", "final"), # CIB10 RPF - "gn1": ("run", "final"), # GNC RPF - "jn1": ("run", "final"), # JNC RPF - "on1": ("run", "final"), # ONC RPF - "tp1": ("run", "final"), # TPC RPF - "ja1": ("run", "final"), # JOG RPF - "tc1": ("run", "final"), # TLM100 RPF - "tl1": ("run", "final"), # TLM50 RPF - "jp2": ("run", "final"), # JPEG2000 MrSID - "sid": ("run", "final"), # MrSID -} - -_latitude_names = {"latitude", "lat"} -_longitude_names = {"longitude", "lon", "lng", "long"} - - -if not _ALLOW_TIME_STEP: - for t, steps in _pages.items(): - steps = list(steps) - if "time" in steps: - steps.remove("time") - _pages[t] = tuple(steps) - -if not _ALLOW_MOSAIC_STEP: - for t, steps in _pages.items(): - steps = list(steps) - if "mosaic" in steps: - steps.remove("mosaic") - _pages[t] = tuple(steps) - -if not _ALLOW_MOSAIC_STEP: - for t, steps in _pages.items(): - steps = list(steps) - if "mosaic" in steps: - steps.remove("mosaic") - _pages[t] = tuple(steps) - - -def get_max_amount_of_steps(): - # We add 1 here to count the save step (implied as first step) - return max([len(page) for page in _pages.values()]) + 1 - - -def get_next_step(upload_session, offset=1): - assert upload_session.upload_type is not None - - if upload_session.completed_step == "error": - return "error" - - try: - pages = _pages[upload_session.upload_type] - except KeyError as e: - raise Exception(_(f"Unsupported file type: {e.message}")) - index = -1 - if upload_session.completed_step and upload_session.completed_step != "save": - index = pages.index(upload_session.completed_step) - return pages[max(min(len(pages) - 1, index + offset), 0)] - - -def _advance_step(req, upload_session): - if upload_session.completed_step != "error": - upload_session.completed_step = get_next_step(upload_session) - else: - return "error" - - -def is_async_step(upload_session): - return _ASYNC_UPLOAD and get_next_step(upload_session, offset=2) == "run" - - -def _get_time_dimensions(layer, upload_session, values=None): - date_time_keywords = ["date", "time", "year", "create", "end", "last", "update", "expire", "enddate"] - - def filter_name(b): - return any([_kw in b.lower() for _kw in date_time_keywords]) - - att_list = [] - try: - dataset_values = values or _get_dataset_values(layer, upload_session, expand=1) - if layer and dataset_values: - ft = dataset_values[0] - attributes = [{"name": k, "binding": ft[k]["binding"] or 0} for k in ft.keys()] - for a in attributes: - if ( - ("Integer" in a["binding"] or "Long" in a["binding"]) and "id" != a["name"].lower() - ) and filter_name(a["name"].lower()): - if dataset_values: - for feat in dataset_values: - if iso8601(str(feat.get(a["name"])["value"])): - if a not in att_list: - att_list.append(a) - elif "Date" in a["binding"]: - att_list.append(a) - elif "String" in a["binding"] and filter_name(a["name"].lower()): - if dataset_values: - for feat in dataset_values: - if feat.get(a["name"])["value"] and iso8601(str(feat.get(a["name"])["value"])): - if a not in att_list: - att_list.append(a) - else: - pass - except Exception: - traceback.print_exc() - return None - return att_list - - -def _fixup_base_file(absolute_base_file, tempdir=None): - if not tempdir or not os.path.exists(tempdir): - tempdir = mkdtemp() - if not os.path.isfile(absolute_base_file): - tmp_files = [f for f in os.listdir(tempdir) if os.path.isfile(os.path.join(tempdir, f))] - for f in tmp_files: - if zipfile.is_zipfile(os.path.join(tempdir, f)): - absolute_base_file = unzip_file(os.path.join(tempdir, f), ".shp", tempdir=tempdir) - absolute_base_file = os.path.join(tempdir, absolute_base_file) - elif zipfile.is_zipfile(absolute_base_file): - absolute_base_file = unzip_file(absolute_base_file, ".shp", tempdir=tempdir) - absolute_base_file = os.path.join(tempdir, absolute_base_file) - if os.path.exists(absolute_base_file): - return absolute_base_file - else: - raise Exception(_(f"File does not exist: {absolute_base_file}")) - - -def _get_dataset_values(layer, upload_session, expand=0): - dataset_values = [] - if upload_session: - try: - absolute_base_file = _fixup_base_file(upload_session.base_file[0].base_file, upload_session.tempdir) - - inDataSource = ogr.Open(absolute_base_file) - lyr = inDataSource.GetLayer(str(layer.name)) - limit = 10 - for feat in islice(lyr, 0, limit): - feat_values = json_loads_byteified(feat.ExportToJson(), upload_session.charset).get("properties") - if feat_values: - for k in feat_values.keys(): - type_code = feat.GetFieldDefnRef(k).GetType() - binding = feat.GetFieldDefnRef(k).GetFieldTypeName(type_code) - feat_value = feat_values[k] if str(feat_values[k]) != "None" else 0 - if expand > 0: - ff = {"value": feat_value, "binding": binding} - feat_values[k] = ff - else: - feat_values[k] = feat_value - dataset_values.append(feat_values) - except Exception as e: - logger.exception(e) - return dataset_values - - -def dataset_eligible_for_time_dimension(request, layer, values=None, upload_session=None): - _is_eligible = False - dataset_values = values or _get_dataset_values(layer, upload_session, expand=0) - att_list = _get_time_dimensions(layer, upload_session) - _is_eligible = att_list or False - if upload_session and _is_eligible: - upload_session.time = True - return (_is_eligible, dataset_values) - - -def run_import(upload_session, async_upload=_ASYNC_UPLOAD): - """Run the import, possibly asynchronously. - - Returns the target datastore. - """ - # run_import can raise an exception which callers should handle - import_session = upload_session.import_session - import_session = gs_uploader.get_session(import_session.id) - if import_session.tasks: - task = import_session.tasks[0] - import_execution_requested = False - if import_session.state == "INCOMPLETE": - if task.state != "ERROR": - raise Exception(_(f"unknown item state: {task.state}")) - elif import_session.state == "PENDING" and task.target.store_type == "coverageStore": - if task.state == "READY": - _log( - f"run_import: async_upload[{async_upload}] Commit Import Session {import_session.id} - target: / - alternate: {task.get_target_layer_name()}" - ) - import_session.commit(async_upload) - import_execution_requested = True - if task.state == "ERROR": - progress = task.get_progress() - raise Exception(_(f"error during import: {progress.get('message')}")) - - # if a target datastore is configured, ensure the datastore exists - # in geoserver and set the uploader target appropriately - - _log(f"run_import: Running Import Session {import_session.id}") - # run async if using a database - if not import_execution_requested: - import_session.commit(async_upload) - - # @todo check status of import session - it may fail, but due to protocol, - # this will not be reported during the commit - import_session = import_session.reload() - return import_session.tasks[0].target - return None - - -def progress_redirect(step, upload_id): - return json_response( - dict( - success=True, - id=upload_id, - redirect_to=f"{reverse('data_upload', args=[step])}?id={upload_id}", - progress=f"{reverse('data_upload_progress')}?id={upload_id}", - ) - ) +from django.core.exceptions import ObjectDoesNotExist +from django.conf import settings def get_max_upload_size(slug): @@ -404,236 +48,68 @@ def get_max_upload_parallelism_limit(slug): return max_number -""" - - ImageMosaics Management -""" - - -def _get_time_regex(spatial_files, base_file_name): - head, tail = os.path.splitext(base_file_name) - - # 1. Look for 'timeregex.properties' files among auxillary_files - regex = None - format = None - for aux in spatial_files[0].auxillary_files: - basename = os.path.basename(aux) - aux_head, aux_tail = os.path.splitext(basename) - if "timeregex" == aux_head and ".properties" == aux_tail: - with open(aux) as timeregex_prop_file: - rr = timeregex_prop_file.read() - if rr and rr.split(","): - rrff = rr.split(",") - regex = rrff[0].split("=")[1] - if len(rrff) > 1: - for rf in rrff: - if "format" in rf: - format = rf.split("=")[1] - break - if regex: - time_regexp = re.compile(regex) - if time_regexp.match(head): - time_tokens = time_regexp.match(head).groups() - if time_tokens: - return regex, format - return None, None - - -def import_imagemosaic_granules( - spatial_files, - append_to_mosaic_opts, - append_to_mosaic_name, - mosaic_time_regex, - mosaic_time_value, - time_presentation, - time_presentation_res, - time_presentation_default_value, - time_presentation_reference_value, -): - # The very first step is to rename the granule by adding the selected regex - # matching value to the filename. - - f = spatial_files[0].base_file - dirname = os.path.dirname(f) - basename = os.path.basename(f) - head, tail = os.path.splitext(basename) - - if not mosaic_time_regex: - mosaic_time_regex, mosaic_time_format = _get_time_regex(spatial_files, basename) - - # 0. A Time Regex is mandartory to validate the files - if not mosaic_time_regex: - raise GeneralUploadException(detail=_("Could not find any valid Time Regex for the Mosaic files.")) - - for spatial_file in spatial_files: - f = spatial_file.base_file - basename = os.path.basename(f) - head, tail = os.path.splitext(basename) - regexp = re.compile(mosaic_time_regex) - if regexp.match(head).groups(): - mosaic_time_value = regexp.match(head).groups()[0] - head = head.replace(regexp.match(head).groups()[0], "{mosaic_time_value}") - if mosaic_time_value: - dst_file = os.path.join(dirname, head.replace("{mosaic_time_value}", mosaic_time_value) + tail) - os.rename(f, dst_file) - spatial_file.base_file = dst_file - - # We use the GeoServer REST APIs in order to create the ImageMosaic - # and later add the granule through the GeoServer Importer. - head = head.replace("{mosaic_time_value}", "") - head = re.sub("^[^a-zA-Z]*|[^a-zA-Z]*$", "", head) - - # 1. Create a zip file containing the ImageMosaic .properties files - # 1a. Let's check and prepare the DB based DataStore - cat = gs_catalog - workspace = cat.get_workspace(settings.DEFAULT_WORKSPACE) - db = ogc_server_settings.datastore_db - db_engine = "postgis" if "postgis" in db["ENGINE"] else db["ENGINE"] +class ImporterRequestAction(enum.Enum): + ROLLBACK = _("rollback") - if not db_engine == "postgis": - raise GeneralUploadException(detail=_("Unsupported DataBase for Mosaics!")) - # dsname = ogc_server_settings.DATASTORE - dsname = db["NAME"] +def error_handler(exc, exec_id=None): + return f'{str(exc.detail if hasattr(exc, "detail") else exc.args[0])}. Request: {exec_id}' - ds_exists = False - try: - ds = get_store(cat, dsname, workspace=workspace) - ds_exists = ds is not None - except FailedRequestError: - ds = cat.create_datastore(dsname, workspace=workspace) - db = ogc_server_settings.datastore_db - db_engine = "postgis" if "postgis" in db["ENGINE"] else db["ENGINE"] - ds.connection_parameters.update( - { - "validate connections": "true", - "max connections": "10", - "min connections": "1", - "fetch size": "1000", - "host": db["HOST"], - "port": db["PORT"] if isinstance(db["PORT"], str) else str(db["PORT"]) or "5432", - "database": db["NAME"], - "user": db["USER"], - "passwd": db["PASSWORD"], - "dbtype": db_engine, - } - ) - cat.save(ds) - ds = get_store(cat, dsname, workspace=workspace) - ds_exists = ds is not None - - if not ds_exists: - raise GeneralUploadException(detail=_("Unsupported DataBase for Mosaics!")) - - context = { - "abs_path_flag": "True", - "time_attr": "time", - "aux_metadata_flag": "False", - "mosaic_time_regex": mosaic_time_regex, - "db_host": db["HOST"], - "db_port": db["PORT"], - "db_name": db["NAME"], - "db_user": db["USER"], - "db_password": db["PASSWORD"], - "db_conn_timeout": db["CONN_TOUT"] if "CONN_TOUT" in db else "10", - "db_conn_min": db["CONN_MIN"] if "CONN_MIN" in db else "1", - "db_conn_max": db["CONN_MAX"] if "CONN_MAX" in db else "5", - "db_conn_validate": db["CONN_VALIDATE"] if "CONN_VALIDATE" in db else "true", - } - indexer_template = """AbsolutePath={abs_path_flag} -Schema= the_geom:Polygon,location:String,{time_attr} -CheckAuxiliaryMetadata={aux_metadata_flag} -SuggestedSPI=it.geosolutions.imageioimpl.plugins.tiff.TIFFImageReaderSpi""" - if mosaic_time_regex: - indexer_template = """AbsolutePath={abs_path_flag} -TimeAttribute={time_attr} -Schema= the_geom:Polygon,location:String,{time_attr}:java.util.Date -PropertyCollectors=TimestampFileNameExtractorSPI[timeregex]({time_attr}) -CheckAuxiliaryMetadata={aux_metadata_flag} -SuggestedSPI=it.geosolutions.imageioimpl.plugins.tiff.TIFFImageReaderSpi""" - - timeregex_template = """regex=(?<=_)({mosaic_time_regex})""" - - if not os.path.exists(f"{dirname}/timeregex.properties"): - with open(f"{dirname}/timeregex.properties", "w") as timeregex_prop_file: - timeregex_prop_file.write(timeregex_template.format(**context)) +class ImporterConcreteManager(GeoServerResourceManager): + """ + The default GeoNode concrete manager, handle the communication with geoserver + For this implementation the interaction with geoserver is not needed + so we are going to overwrite the concrete manager to avoid it + """ - datastore_template = r"""SPI=org.geotools.data.postgis.PostgisNGDataStoreFactory -host={db_host} -port={db_port} -database={db_name} -user={db_user} -passwd={db_password} -Loose\ bbox=true -Estimated\ extends=false -validate\ connections={db_conn_validate} -Connection\ timeout={db_conn_timeout} -min\ connections={db_conn_min} -max\ connections={db_conn_max}""" + def copy(self, instance, uuid, defaults): + return ResourceBase.objects.get(uuid=uuid) - if not os.path.exists(f"{dirname}/indexer.properties"): - with open(f"{dirname}/indexer.properties", "w") as indexer_prop_file: - indexer_prop_file.write(indexer_template.format(**context)) + def update(self, uuid, **kwargs) -> ResourceBase: + return ResourceBase.objects.get(uuid=uuid) - if not os.path.exists(f"{dirname}/datastore.properties"): - with open(f"{dirname}/datastore.properties", "w") as datastore_prop_file: - datastore_prop_file.write(datastore_template.format(**context)) - files_to_upload = [] - if not append_to_mosaic_opts and spatial_files: - z = zipfile.ZipFile(f"{dirname}/{head}.zip", "w", allowZip64=True) - for spatial_file in spatial_files: - f = spatial_file.base_file - dst_basename = os.path.basename(f) - dst_head, dst_tail = os.path.splitext(dst_basename) - if not files_to_upload: - # Let's import only the first granule - z.write(spatial_file.base_file, arcname=dst_head + dst_tail) - files_to_upload.append(spatial_file.base_file) - if os.path.exists(f"{dirname}/indexer.properties"): - z.write(f"{dirname}/indexer.properties", arcname="indexer.properties") - if os.path.exists(f"{dirname}/datastore.properties"): - z.write(f"{dirname}/datastore.properties", arcname="datastore.properties") - if mosaic_time_regex: - z.write(f"{dirname}/timeregex.properties", arcname="timeregex.properties") - z.close() +custom_resource_manager = ResourceManager(concrete_manager=ImporterConcreteManager()) - # 2. Send a "create ImageMosaic" request to GeoServer through gs_config - # - name = name of the ImageMosaic (equal to the base_name) - # - data = abs path to the zip file - # - configure = parameter allows for future configuration after harvesting - name = head - with open(f"{dirname}/{head}.zip", "rb") as data: - try: - cat.create_imagemosaic(name, data) - except ConflictingDataError: - # Trying to append granules to an existing mosaic - pass +def call_rollback_function( + execution_id, + handlers_module_path, + prev_action, + layer=None, + alternate=None, + error=None, + **kwargs, +): + from geonode.upload.celery_tasks import import_orchestrator + + task_params = ( + {}, + execution_id, + handlers_module_path, + "start_rollback", + layer, + alternate, + ImporterRequestAction.ROLLBACK.value, + ) + kwargs["previous_action"] = prev_action + kwargs["error"] = error_handler(error, exec_id=execution_id) + import_orchestrator.apply_async(task_params, kwargs) - # configure time as LIST - if mosaic_time_regex: - set_time_dimension( - cat, - name, - workspace, - time_presentation, - time_presentation_res, - time_presentation_default_value, - time_presentation_reference_value, - ) - # - since GeoNode will upload the first granule again through the Importer, we need to / - # delete the one created by the gs_config - # mosaic_delete_first_granule(cat, name) - if len(spatial_files) > 1: - spatial_files = spatial_files[0] - return head, files_to_upload - else: - cat._cache.clear() - cat.reset() - # cat.reload() - return append_to_mosaic_name, files_to_upload +def find_key_recursively(obj, key): + """ + Celery (unluckly) append the kwargs for each task + under a new kwargs key, so sometimes is faster + to look into the key recursively instead of + parsing the dict + """ + if key in obj: + return obj.get(key, None) + for _unsed, v in obj.items(): + if isinstance(v, dict): + return find_key_recursively(v, key) class UploadLimitValidator: diff --git a/geonode/upload/views.py b/geonode/upload/views.py index 691e105b871..50b39831df7 100644 --- a/geonode/upload/views.py +++ b/geonode/upload/views.py @@ -1,6 +1,6 @@ ######################################################################### # -# Copyright (C) 2016 OSGeo +# Copyright (C) 2024 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 @@ -16,40 +16,3 @@ # along with this program. If not, see . # ######################################################################### -""" -Provide views for doing an upload. - -The upload process may be multi step so views are all handled internally here -by the view function. - -The pattern to support separation of view/logic is each step in the upload -process is suffixed with "_step". The view for that step is suffixed with -"_step_view". The goal of separation of view/logic is to support various -programmatic uses of this API. The logic steps should not accept request objects -or return response objects. - -State is stored in a UploaderSession object stored in the user's session. -This needs to be made more stateful by adding a model. -""" -import logging -from django.shortcuts import get_object_or_404 -from django.core.exceptions import PermissionDenied -from django.contrib.auth.decorators import login_required - -from .models import Upload -from .utils import json_response - -logger = logging.getLogger(__name__) - - -@login_required -def delete(req, id): - upload = get_object_or_404(Upload, id=id) - if (not req.user.is_superuser and req.user != upload.user) or not req.user.is_authenticated: - raise PermissionDenied() - upload.delete() - return json_response( - dict( - success=True, - ) - ) diff --git a/geonode/urls.py b/geonode/urls.py index 99b1e5ebaf9..f65f402f461 100644 --- a/geonode/urls.py +++ b/geonode/urls.py @@ -32,6 +32,7 @@ from django.contrib.sitemaps.views import sitemap import geonode.proxy.urls +from geonode.upload.api.views import ImporterViewSet, ResourceImporter from . import views from . import version @@ -123,6 +124,11 @@ re_path(r"^api/roles", roles, name="roles"), re_path(r"^api/adminRole", admin_role, name="adminRole"), re_path(r"^api/users", users, name="users"), + re_path( + r"api/v2/resources/(?P\w+)/copy", + ResourceImporter.as_view({"put": "copy"}), + name="importer_resource_copy", + ), re_path(r"^api/v2/", include(router.urls)), re_path(r"^api/v2/", include("geonode.api.urls")), re_path(r"^api/v2/", include("geonode.management_commands_http.urls")), @@ -130,6 +136,11 @@ re_path(r"^api/v2/", include("geonode.facets.urls")), re_path(r"^api/v2/", include("geonode.assets.urls")), re_path(r"", include(api.urls)), + re_path( + r"uploads/upload", + ImporterViewSet.as_view({"post": "create"}), + name="importer_upload", + ), ] # tinymce WYSIWYG HTML Editor @@ -195,10 +206,6 @@ # Serve static files urlpatterns += staticfiles_urlpatterns() urlpatterns += static(settings.LOCAL_MEDIA_URL, document_root=settings.MEDIA_ROOT) -handler401 = "geonode.views.err403" -handler403 = "geonode.views.err403" -handler404 = "geonode.views.handler404" -handler500 = "geonode.views.handler500" if settings.MONITORING_ENABLED: diff --git a/geonode/utils.py b/geonode/utils.py index 9268feb51da..514061ca7f0 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -18,30 +18,25 @@ ######################################################################### import os -import gc import re import json import time import base64 -import select import shutil -import string import typing import logging -import tarfile import datetime import requests import tempfile import ipaddress import itertools import traceback -import subprocess from lxml import etree from osgeo import ogr from PIL import Image from urllib3 import Retry -from io import BytesIO, StringIO +from io import BytesIO from decimal import Decimal from threading import local from slugify import slugify @@ -49,7 +44,7 @@ from requests.exceptions import RetryError from collections import namedtuple, defaultdict from rest_framework.exceptions import APIException -from math import atan, exp, log, pi, sin, tan, floor +from math import atan, exp, log, pi, tan from zipfile import ZipFile, is_zipfile, ZIP_DEFLATED from geonode.upload.api.exceptions import GeneralUploadException @@ -85,23 +80,14 @@ unquote, urlparse, urlsplit, - urlencode, urlunparse, - parse_qsl, - ParseResult, ) MAX_EXTENT = 20037508.34 FULL_ROTATION_DEG = 360.0 HALF_ROTATION_DEG = 180.0 -DEFAULT_TITLE = "" -DEFAULT_ABSTRACT = "" -INVALID_PERMISSION_MESSAGE = _("Invalid permission level.") -ALPHABET = f"{string.ascii_uppercase + string.ascii_lowercase + string.digits}-_" -ALPHABET_REVERSE = {c: i for (i, c) in enumerate(ALPHABET)} -BASE = len(ALPHABET) SIGN_CHARACTER = "$" SQL_PARAMS_RE = re.compile(r"%\(([\w_\-]+)\)s") @@ -112,20 +98,6 @@ requests.packages.urllib3.disable_warnings() -signalnames = [ - "class_prepared", - "m2m_changed", - "post_delete", - "post_init", - "post_save", - "post_syncdb", - "pre_delete", - "pre_init", - "pre_save", -] -signals_store = {} - -id_none = id(None) logger = logging.getLogger("geonode.utils") @@ -316,23 +288,6 @@ def unzip_file(upload_file, extension=".shp", tempdir=None): return absolute_base_file -def extract_tarfile(upload_file, extension=".shp", tempdir=None): - """ - Extracts a tarfile into a temporary directory and returns the full path of the .shp file inside (if any) - """ - absolute_base_file = None - if tempdir is None: - tempdir = mkdtemp() - - the_tar = tarfile.open(upload_file) - the_tar.extractall(tempdir) - for item in the_tar.getnames(): - if item.endswith(extension): - absolute_base_file = os.path.join(tempdir, item) - - return absolute_base_file - - def get_dataset_name(dataset): """Get the workspace where the input layer belongs""" _name = dataset.name @@ -462,36 +417,6 @@ def _get_basic_auth_info(request): return username, password -def batch_delete(request): - # TODO - pass - - -def _split_query(query): - """ - split and strip keywords, preserve space - separated quoted blocks. - """ - - qq = query.split(" ") - keywords = [] - accum = None - for kw in qq: - if accum is None: - if kw.startswith('"'): - accum = kw[1:] - elif kw: - keywords.append(kw) - else: - accum += f" {kw}" - if kw.endswith('"'): - keywords.append(accum[0:-1]) - accum = None - if accum is not None: - keywords.append(accum) - return [kw.strip() for kw in keywords if kw.strip()] - - # Swaps coords order from xmin,ymin,xmax,ymax to xmin,xmax,ymin,ymax and viceversa def bbox_swap(bbox): _bbox = [float(o) for o in bbox] @@ -613,51 +538,12 @@ def bbox_to_projection(native_bbox, target_srid=4326): return native_bbox -def bounds_to_zoom_level(bounds, width, height): - WORLD_DIM = {"height": 256.0, "width": 256.0} - ZOOM_MAX = 21 - - def latRad(lat): - _sin = sin(lat * pi / HALF_ROTATION_DEG) - if abs(_sin) != 1.0: - radX2 = log((1.0 + _sin) / (1.0 - _sin)) / 2.0 - else: - radX2 = log(1.0) / 2.0 - return max(min(radX2, pi), -pi) / 2.0 - - def zoom(mapPx, worldPx, fraction): - try: - return floor(log(mapPx / worldPx / fraction) / log(2.0)) - except Exception: - return 0 - - ne = [float(bounds[2]), float(bounds[3])] - sw = [float(bounds[0]), float(bounds[1])] - latFraction = (latRad(ne[1]) - latRad(sw[1])) / pi - lngDiff = ne[0] - sw[0] - lngFraction = ((lngDiff + FULL_ROTATION_DEG) if lngDiff < 0 else lngDiff) / FULL_ROTATION_DEG - latZoom = zoom(float(height), WORLD_DIM["height"], latFraction) - lngZoom = zoom(float(width), WORLD_DIM["width"], lngFraction) - # ratio = float(max(width, height)) / float(min(width, height)) - # z_offset = 0 if ratio >= 2 else -1 - z_offset = 0 - zoom = int(max(latZoom, lngZoom) + z_offset) - zoom = 0 if zoom > ZOOM_MAX else zoom - return max(zoom, 0) - - def llbbox_to_mercator(llbbox): minlonlat = forward_mercator([llbbox[0], llbbox[2]]) maxlonlat = forward_mercator([llbbox[1], llbbox[3]]) return [minlonlat[0], minlonlat[1], maxlonlat[0], maxlonlat[1]] -def mercator_to_llbbox(bbox): - minlonlat = inverse_mercator([bbox[0], bbox[2]]) - maxlonlat = inverse_mercator([bbox[1], bbox[3]]) - return [minlonlat[0], minlonlat[1], maxlonlat[0], maxlonlat[1]] - - def forward_mercator(lonlat): """ Given geographic coordinates, return a x,y tuple in spherical mercator. @@ -789,39 +675,6 @@ def json_response(body=None, errors=None, url=None, redirect_to=None, exception= return HttpResponse(body, content_type=content_type, status=status) -def num_encode(n): - if n < 0: - return SIGN_CHARACTER + num_encode(-n) - s = [] - while True: - n, r = divmod(n, BASE) - s.append(ALPHABET[r]) - if n == 0: - break - return "".join(reversed(s)) - - -def num_decode(s): - if s[0] == SIGN_CHARACTER: - return -num_decode(s[1:]) - n = 0 - for c in s: - n = n * BASE + ALPHABET_REVERSE[c] - return n - - -def format_urls(a, values): - b = [] - for i in a: - j = i.copy() - try: - j["url"] = str(j["url"]).format(**values) - except KeyError: - j["url"] = None - b.append(j) - return b - - def build_abstract(resourcebase, url=None, includeURL=True): if resourcebase.abstract and url and includeURL: return f"{resourcebase.abstract} -- [{url}]({url})" @@ -843,31 +696,6 @@ def build_caveats(resourcebase): return "" -def build_social_links(request, resourcebase): - netschema = "https" if request.is_secure() else "http" - host = request.get_host() - path = request.get_full_path() - social_url = f"{netschema}://{host}{path}" - # Don't use datetime strftime() because it requires year >= 1900 - # see - # https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior - date = "{0.month:02d}/{0.day:02d}/{0.year:4d}".format(resourcebase.date) if resourcebase.date else None - abstract = build_abstract(resourcebase, url=social_url, includeURL=True) - caveats = build_caveats(resourcebase) - hashtags = ",".join(getattr(settings, "TWITTER_HASHTAGS", [])) - return format_urls( - settings.SOCIAL_ORIGINS, - { - "name": resourcebase.title, - "date": date, - "abstract": abstract, - "caveats": caveats, - "hashtags": hashtags, - "url": social_url, - }, - ) - - def check_shp_columnnames(layer): """Check if shapefile for a given layer has valid column names. If not, try to fix column names and warn the user @@ -988,25 +816,6 @@ def fixup_shp_columnnames(inShapefile, charset, tempdir=None): return True, None, list_col -def id_to_obj(id_): - if id_ == id_none: - return None - - for obj in gc.get_objects(): - if id(obj) == id_: - return obj - raise Exception("Not found") - - -def printsignals(): - for signalname in signalnames: - logger.debug(f"SIGNALNAME: {signalname}") - signaltype = getattr(models.signals, signalname) - signals = signaltype.receivers[:] - for signal in signals: - logger.debug(signal) - - class DisableDjangoSignals: """ Python3 class temporarily disabling django signals on model creation. @@ -1050,31 +859,6 @@ def reconnect(self, signal): del self.stashed_signals[signal] -def run_subprocess(*cmd, **kwargs): - p = subprocess.Popen(" ".join(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) - stdout = StringIO() - stderr = StringIO() - buff_size = 1024 - while p.poll() is None: - inr = [p.stdout.fileno(), p.stderr.fileno()] - inw = [] - rlist, wlist, xlist = select.select(inr, inw, []) - - for r in rlist: - if r == p.stdout.fileno(): - readfrom = p.stdout - readto = stdout - else: - readfrom = p.stderr - readto = stderr - readto.write(readfrom.read(buff_size)) - - for w in wlist: - w.write("") - - return p.returncode, stdout.getvalue(), stderr.getvalue() - - def parse_datetime(value): for patt in settings.DATETIME_INPUT_FORMATS: try: @@ -1734,44 +1518,6 @@ def set_resource_default_links(instance, layer, prune=False, **kwargs): logger.exception(e) -def add_url_params(url, params): - """Add GET params to provided URL being aware of existing. - - :param url: string of target URL - :param params: dict containing requested params to be added - :return: string with updated URL - - >> url = 'http://stackoverflow.com/test?answers=true' - >> new_params = {'answers': False, 'data': ['some','values']} - >> add_url_params(url, new_params) - 'http://stackoverflow.com/test?data=some&data=values&answers=false' - """ - # Unquoting URL first so we don't loose existing args - url = unquote(url) - # Extracting url info - parsed_url = urlparse(url) - # Extracting URL arguments from parsed URL - get_args = parsed_url.query - # Converting URL arguments to dict - parsed_get_args = dict(parse_qsl(get_args)) - # Merging URL arguments dict with new params - parsed_get_args.update(params) - - # Bool and Dict values should be converted to json-friendly values - # you may throw this part away if you don't like it :) - parsed_get_args.update({k: json.dumps(v) for k, v in parsed_get_args.items() if isinstance(v, (bool, dict))}) - - # Converting URL argument to proper query string - encoded_get_args = urlencode(parsed_get_args, doseq=True) - # Creating new parsed result object based on provided with new - # URL arguments. Same thing happens inside of urlparse. - new_url = ParseResult( - parsed_url.scheme, parsed_url.netloc, parsed_url.path, parsed_url.params, encoded_get_args, parsed_url.fragment - ).geturl() - - return new_url - - json_serializer_k_map = { "user": settings.AUTH_USER_MODEL, "owner": settings.AUTH_USER_MODEL, diff --git a/geonode/views.py b/geonode/views.py index 8dc0dbe8bef..de1b82ccdf5 100644 --- a/geonode/views.py +++ b/geonode/views.py @@ -25,7 +25,6 @@ from django.db.models import Q from django.urls import reverse from django.conf import settings -from django.shortcuts import render from django.template.response import TemplateResponse from geonode.base.templatetags.base_tags import facets from django.http import HttpResponse, HttpResponseRedirect @@ -100,18 +99,6 @@ def err403(request, exception): return TemplateResponse(request, "401.html", {}, status=401).render() -def handler404(request, exception, template_name="404.html"): - response = render(request, template_name) - response.status_code = 404 - return response - - -def handler500(request, template_name="500.html"): - response = render(request, template_name) - response.status_code = 500 - return response - - def ident_json(request): site_url = settings.SITEURL.rstrip("/") if settings.SITEURL.startswith("http") else settings.SITEURL json_data = {} diff --git a/package/debian/changelog b/package/debian/changelog index 18b218405c1..f3f977dd5fe 100644 --- a/package/debian/changelog +++ b/package/debian/changelog @@ -3948,7 +3948,7 @@ geonode (2.4.0+beta26) trusty; urgency=high * [85e635] add geonode support in Readme [ Tyler Garner ] - * [98d398] support GeoJSON upload through importer. Relies on 2.6+ fix https://github.com/geoserver/geoserver/commit/a7efe793d3b1025eceb41c899f381a5b16df5b9b + * [98d398] support GeoJSON upload through geonode.upload. Relies on 2.6+ fix https://github.com/geoserver/geoserver/commit/a7efe793d3b1025eceb41c899f381a5b16df5b9b [ Matt Bertrand ] * [7fd2d6] Add owner filter to haystack search diff --git a/requirements.txt b/requirements.txt index b21407db133..0d8b8a9399c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -88,7 +88,6 @@ geonode-pinax-notifications==6.0.0.2 # GeoNode org maintained apps. # django-geonode-mapstore-client==4.0.5 git+https://github.com/GeoNode/geonode-mapstore-client.git@master#egg=django_geonode_mapstore_client -git+https://github.com/GeoNode/geonode-importer.git@master#egg=geonode-importer django-avatar==8.0.0 geonode-oauth-toolkit==2.2.2.2 geonode-user-messages==2.0.2.2 @@ -99,6 +98,14 @@ geoserver-restconfig~=2.0.12 gn-gsimporter==2.0.4 gisdata==0.5.4 +# importer dependencies + +setuptools>=59 +gdal<=3.4.3 +pdok-geopackage-validator==0.8.5 +geonode-django-dynamic-model==0.4.0 + + # datetimepicker widget django-bootstrap3-datetimepicker-2==2.8.3 diff --git a/setup.cfg b/setup.cfg index 94b5c655a11..c5c3a71400e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -113,7 +113,6 @@ install_requires = # GeoNode org maintained apps. django-geonode-mapstore-client>=4.0.5,<5.0.0 - geonode-importer>=1.0.2 django-avatar==8.0.0 geonode-oauth-toolkit==2.2.2.2 geonode-user-messages==2.0.2.2 diff --git a/test.sh b/test.sh index 433d4f6390b..d9987fd1849 100755 --- a/test.sh +++ b/test.sh @@ -5,4 +5,4 @@ set -a set +a paver setup_data -coverage run --branch --source=geonode manage.py test -v 3 --keepdb $@ +coverage run --branch --source=geonode manage.py test -v 3 --keepdb $@ \ No newline at end of file From d16fad4d4d3aedcddb1867e7cd05b7e5a07cdcf3 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:16:07 +0200 Subject: [PATCH 18/61] bump GeoNode to version 5.0.0 (#12656) --- geonode/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geonode/__init__.py b/geonode/__init__.py index 4f90e75c17b..a9ee4c5a606 100644 --- a/geonode/__init__.py +++ b/geonode/__init__.py @@ -19,7 +19,7 @@ import os -__version__ = (4, 4, 0, "dev", 0) +__version__ = (5, 0, 0, "dev", 0) def get_version(): From fbd853db6e9e3adf11786f42bd05c78c0d149909 Mon Sep 17 00:00:00 2001 From: Henning Bredel <881756+ridoo@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:14:42 +0200 Subject: [PATCH 19/61] [Fixes #12639] Adds all assigned group permissions to user (#12640) * Adds all assigned group permissions to user * make set json serializable --- geonode/people/models.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/geonode/people/models.py b/geonode/people/models.py index d115d2c85f3..dd4fcceef64 100644 --- a/geonode/people/models.py +++ b/geonode/people/models.py @@ -197,23 +197,29 @@ def location(self): @property def perms(self): + perms = set() if self.is_superuser or self.is_staff: # return all permissions for admins - perms = PERMISSIONS.values() - else: - user_groups = self.groups.values_list("name", flat=True) - group_perms = ( - Permission.objects.filter(group__name__in=user_groups).distinct().values_list("codename", flat=True) - ) - # return constant names defined by GeoNode - perms = [PERMISSIONS[db_perm] for db_perm in group_perms] + perms.update(PERMISSIONS.values()) + + user_groups = self.groups.values_list("name", flat=True) + group_perms = ( + Permission.objects.filter(group__name__in=user_groups).distinct().values_list("codename", flat=True) + ) + for p in group_perms: + if p in PERMISSIONS: + # return constant names defined by GeoNode + perms.add(PERMISSIONS[p]) + else: + # add custom permissions + perms.add(p) # check READ_ONLY mode config = Configuration.load() if config.read_only: # exclude permissions affected by readonly perms = [perm for perm in perms if perm not in READ_ONLY_AFFECTED_PERMISSIONS] - return perms + return list(perms) def save(self, *args, **kwargs): super().save(*args, **kwargs) From 8d5800499eca2eb91fe43467d1881f2c5f5a3119 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:19:14 +0200 Subject: [PATCH 20/61] [Fixes #12674] store spatial file is always false during cloning (#12675) * [Fixes #12674] store spatial file is always false during cloning * [Fixes #12674] store spatial file is always false during cloning * [Fixes #12674] store spatial file is always false during cloning * Update requirements.txt --- .env_dev | 3 --- geonode/upload/handlers/common/raster.py | 2 +- geonode/upload/handlers/common/remote.py | 2 +- geonode/upload/handlers/common/vector.py | 4 ++-- geonode/upload/handlers/shapefile/handler.py | 2 +- geonode/upload/handlers/tiles3d/handler.py | 2 +- geonode/upload/handlers/tiles3d/tests.py | 2 +- 7 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.env_dev b/.env_dev index 34f85b41da6..d37591416fc 100644 --- a/.env_dev +++ b/.env_dev @@ -166,9 +166,6 @@ DEBUG=True SECRET_KEY='myv-y4#7j-d*p-__@j#*3z@!y24fz8%^z2v6atuy4bo9vqr1_a' -# STATIC_ROOT=/mnt/volumes/statics/static/ -# MEDIA_ROOT=/mnt/volumes/statics/uploaded/ -# GEOIP_PATH=/mnt/volumes/statics/geoip.db CACHE_BUSTING_STATIC_ENABLED=False diff --git a/geonode/upload/handlers/common/raster.py b/geonode/upload/handlers/common/raster.py index 14dd5d40971..e581d739d1f 100644 --- a/geonode/upload/handlers/common/raster.py +++ b/geonode/upload/handlers/common/raster.py @@ -125,7 +125,7 @@ def extract_params_from_data(_data, action=None): """ if action == exa.COPY.value: title = json.loads(_data.get("defaults")) - return {"title": title.pop("title")}, _data + return {"title": title.pop("title"), "store_spatial_file": True}, _data return { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), diff --git a/geonode/upload/handlers/common/remote.py b/geonode/upload/handlers/common/remote.py index 6cc827f0e15..f72579e2708 100755 --- a/geonode/upload/handlers/common/remote.py +++ b/geonode/upload/handlers/common/remote.py @@ -102,7 +102,7 @@ def extract_params_from_data(_data, action=None): """ if action == exa.COPY.value: title = json.loads(_data.get("defaults")) - return {"title": title.pop("title")}, _data + return {"title": title.pop("title"), "store_spatial_file": True}, _data return { "source": _data.pop("source", "upload"), diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index ddf755d55a1..3aa5a48040c 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -135,7 +135,7 @@ def extract_params_from_data(_data, action=None): """ if action == exa.COPY.value: title = json.loads(_data.get("defaults")) - return {"title": title.pop("title")}, _data + return {"title": title.pop("title"), "store_spatial_file": True}, _data return { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), @@ -258,7 +258,7 @@ def perform_last_step(execution_id): that the execution is completed """ _exec = BaseHandler.perform_last_step(execution_id=execution_id) - if _exec and not _exec.input_params.get("store_spatial_file", False): + if _exec and not _exec.input_params.get("store_spatial_file", True): resources = ResourceHandlerInfo.objects.filter(execution_request=_exec) # getting all assets list assets = filter(None, [get_default_asset(x.resource) for x in resources]) diff --git a/geonode/upload/handlers/shapefile/handler.py b/geonode/upload/handlers/shapefile/handler.py index d93214a03bc..1f5951c32b8 100644 --- a/geonode/upload/handlers/shapefile/handler.py +++ b/geonode/upload/handlers/shapefile/handler.py @@ -103,7 +103,7 @@ def extract_params_from_data(_data, action=None): """ if action == exa.COPY.value: title = json.loads(_data.get("defaults")) - return {"title": title.pop("title")}, _data + return {"title": title.pop("title"), "store_spatial_file": True}, _data additional_params = { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), diff --git a/geonode/upload/handlers/tiles3d/handler.py b/geonode/upload/handlers/tiles3d/handler.py index 9cb2db7e426..43cbe1ba498 100755 --- a/geonode/upload/handlers/tiles3d/handler.py +++ b/geonode/upload/handlers/tiles3d/handler.py @@ -146,7 +146,7 @@ def extract_params_from_data(_data, action=None): """ if action == exa.COPY.value: title = json.loads(_data.get("defaults")) - return {"title": title.pop("title")}, _data + return {"title": title.pop("title"), "store_spatial_file": True}, _data return { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), diff --git a/geonode/upload/handlers/tiles3d/tests.py b/geonode/upload/handlers/tiles3d/tests.py index 4db2b54d6b0..8bbfc66cb2e 100755 --- a/geonode/upload/handlers/tiles3d/tests.py +++ b/geonode/upload/handlers/tiles3d/tests.py @@ -99,7 +99,7 @@ def test_extract_params_from_data(self): action="copy", ) - self.assertEqual(actual, {"title": "title_of_the_cloned_resource"}) + self.assertEqual(actual, {"store_spatial_file": True, "title": "title_of_the_cloned_resource"}) def test_is_valid_should_raise_exception_if_the_3dtiles_is_invalid(self): data = {"base_file": "/using/double/dot/in/the/name/is/an/error/file.invalid.json"} From 883f9333ef373bf4b939a7a1a42bf8f471fb7602 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:37:34 +0200 Subject: [PATCH 21/61] [Fixes #12657] Align supported_file_extension_config with the new client configuration (#12673) * [Fixes #12657] Rename ACTIONS into TASKS * [Fixes #12657] Refactor supported types * [Fixes #12657] Refactor supported types * [Fixes #12657] Refactor supported type, fix data retriever and refactor handlers configuration * [Fixes #12657] Refactor supported type, fix data retriever and refactor handlers configuration * [Fixes #12657] Refactor supported type, fix data retriever and refactor handlers configuration * [Fixes #12657] Refactor supported type, fix data retriever and refactor handlers configuration * [Fixes #12657] Refactor supported type, fix data retriever and refactor handlers configuration * [Fixes #12657] Refactor supported type, fix data retriever and refactor handlers configuration --- .../management/commands/importlayers.py | 1 + geonode/resource/api/tests.py | 4 +- geonode/resource/enumerator.py | 1 + ...remove_executionrequest_source_and_more.py | 37 +++++++++ geonode/resource/models.py | 2 - geonode/security/tests.py | 4 +- geonode/settings.py | 50 ------------- geonode/storage/tests.py | 17 ----- geonode/tests/test_utils.py | 37 +-------- geonode/upload/api/serializer.py | 4 +- geonode/upload/api/tests.py | 10 ++- geonode/upload/api/views.py | 9 +-- geonode/upload/celery_tasks.py | 6 +- geonode/upload/handlers/README.md | 4 +- geonode/upload/handlers/apps.py | 18 ----- geonode/upload/handlers/base.py | 12 +-- geonode/upload/handlers/common/metadata.py | 23 +----- geonode/upload/handlers/common/raster.py | 22 ++---- geonode/upload/handlers/common/remote.py | 8 +- geonode/upload/handlers/common/serializer.py | 4 +- geonode/upload/handlers/common/test_remote.py | 10 +-- geonode/upload/handlers/common/vector.py | 46 ++++++++++-- geonode/upload/handlers/csv/handler.py | 35 +++------ geonode/upload/handlers/csv/tests.py | 10 +-- geonode/upload/handlers/geojson/handler.py | 40 ++++------ geonode/upload/handlers/geojson/tests.py | 10 +-- geonode/upload/handlers/geotiff/handler.py | 41 ++++++++-- geonode/upload/handlers/geotiff/tests.py | 10 +-- geonode/upload/handlers/gpkg/handler.py | 21 ++++-- geonode/upload/handlers/gpkg/tests.py | 10 +-- geonode/upload/handlers/kml/handler.py | 16 ++-- geonode/upload/handlers/kml/tests.py | 10 +-- .../handlers/remote/tests/test_3dtiles.py | 10 +-- .../upload/handlers/remote/tests/test_wms.py | 14 ++-- geonode/upload/handlers/remote/tiles3d.py | 4 + geonode/upload/handlers/shapefile/handler.py | 46 +++++------- .../upload/handlers/shapefile/serializer.py | 4 +- geonode/upload/handlers/shapefile/tests.py | 10 +-- geonode/upload/handlers/sld/handler.py | 36 ++++----- geonode/upload/handlers/sld/tests.py | 6 +- geonode/upload/handlers/tiles3d/handler.py | 20 +++-- geonode/upload/handlers/tiles3d/tests.py | 20 +++-- geonode/upload/handlers/utils.py | 7 -- geonode/upload/handlers/xml/handler.py | 36 ++++----- geonode/upload/handlers/xml/serializer.py | 4 +- geonode/upload/handlers/xml/tests.py | 6 +- geonode/upload/orchestrator.py | 23 ++++-- geonode/upload/tests/end2end/test_end2end.py | 75 ++++++------------- .../upload/tests/end2end/test_end2end_copy.py | 15 ++-- geonode/upload/tests/unit/test_dastore.py | 6 +- .../upload/tests/unit/test_orchestrator.py | 20 ++--- geonode/upload/tests/unit/test_publisher.py | 4 +- geonode/upload/tests/unit/test_task.py | 16 ++-- geonode/upload/utils.py | 3 + geonode/utils.py | 39 ++++++++-- 55 files changed, 443 insertions(+), 513 deletions(-) create mode 100644 geonode/resource/migrations/0009_remove_executionrequest_source_and_more.py diff --git a/geonode/geoserver/management/commands/importlayers.py b/geonode/geoserver/management/commands/importlayers.py index b92ac407396..c6eb9e233b3 100644 --- a/geonode/geoserver/management/commands/importlayers.py +++ b/geonode/geoserver/management/commands/importlayers.py @@ -149,6 +149,7 @@ def execute(self): params[name] = os.path.basename(value.name) params["non_interactive"] = 'true' + params["action"] = 'upload' response = client.post( urljoin(self.host, "/api/v2/uploads/upload/"), auth=HTTPBasicAuth(self.username, self.password), diff --git a/geonode/resource/api/tests.py b/geonode/resource/api/tests.py index 1deaa3f9a47..72756f2179a 100644 --- a/geonode/resource/api/tests.py +++ b/geonode/resource/api/tests.py @@ -157,9 +157,7 @@ def test_endpoint_should_raise_error_if_pk_is_not_passed(self): def test_endpoint_should_return_the_source(self): # creating dummy execution request - obj = ExecutionRequest.objects.create( - user=self.superuser, func_name="import_new_resource", action="import", source="upload_workflow" - ) + obj = ExecutionRequest.objects.create(user=self.superuser, func_name="import_new_resource", action="upload") self.client.force_login(self.superuser) _url = f"{reverse('executionrequest-list')}/{obj.exec_id}" diff --git a/geonode/resource/enumerator.py b/geonode/resource/enumerator.py index 8ace0c22100..4d96b0aa291 100644 --- a/geonode/resource/enumerator.py +++ b/geonode/resource/enumerator.py @@ -22,6 +22,7 @@ class ExecutionRequestAction(enum.Enum): IMPORT = _("import") + UPLOAD = _("upload") CREATE = _("create") COPY = _("copy") DELETE = _("delete") diff --git a/geonode/resource/migrations/0009_remove_executionrequest_source_and_more.py b/geonode/resource/migrations/0009_remove_executionrequest_source_and_more.py new file mode 100644 index 00000000000..c547801136b --- /dev/null +++ b/geonode/resource/migrations/0009_remove_executionrequest_source_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.9 on 2024-10-18 10:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("resource", "0008_executionrequest_source"), + ] + + operations = [ + migrations.RemoveField( + model_name="executionrequest", + name="source", + ), + migrations.AlterField( + model_name="executionrequest", + name="action", + field=models.CharField( + choices=[ + ("import", "import"), + ("upload", "upload"), + ("create", "create"), + ("copy", "copy"), + ("delete", "delete"), + ("permissions", "permissions"), + ("update", "update"), + ("ingest", "ingest"), + ("unknown", "unknown"), + ], + default="unknown", + max_length=50, + null=True, + ), + ), + ] diff --git a/geonode/resource/models.py b/geonode/resource/models.py index 791b9f65d0f..f6993492225 100644 --- a/geonode/resource/models.py +++ b/geonode/resource/models.py @@ -60,5 +60,3 @@ class ExecutionRequest(models.Model): action = models.CharField( max_length=50, choices=ACTION_CHOICES, default=ExecutionRequestAction.UNKNOWN.value, null=True ) - - source = models.CharField(max_length=250, null=True, default=None) diff --git a/geonode/security/tests.py b/geonode/security/tests.py index d2707531bad..2c634ecabea 100644 --- a/geonode/security/tests.py +++ b/geonode/security/tests.py @@ -752,9 +752,7 @@ def test_dataset_permissions(self): bobby = get_user_model().objects.get(username="bobby") self.client.force_login(get_user_model().objects.get(username="admin")) - payload = { - "base_file": open(f"{project_dir}/tests/fixture/valid.geojson", "rb"), - } + payload = {"base_file": open(f"{project_dir}/tests/fixture/valid.geojson", "rb"), "action": "upload"} response = self.client.post(reverse("importer_upload"), data=payload) layer = ResourceHandlerInfo.objects.filter(execution_request=response.json()["execution_id"]).first().resource if layer is None: diff --git a/geonode/settings.py b/geonode/settings.py index 03807b16dbc..2be775af909 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -2240,56 +2240,6 @@ def get_geonode_catalogue_service(): "document_upload", ) -SUPPORTED_DATASET_FILE_TYPES = [ - { - "id": "shp", - "label": "ESRI Shapefile", - "format": "vector", - "ext": ["shp"], - "requires": ["shp", "prj", "dbf", "shx"], - "optional": ["xml", "sld"], - }, - { - "id": "tiff", - "label": "GeoTIFF", - "format": "raster", - "ext": ["tiff", "tif", "geotiff", "geotif"], - "mimeType": ["image/tiff"], - "optional": ["xml", "sld"], - }, - { - "id": "csv", - "label": "Comma Separated Value (CSV)", - "format": "vector", - "ext": ["csv"], - "mimeType": ["text/csv"], - "optional": ["xml", "sld"], - }, - { - "id": "zip", - "label": "Zip Archive", - "format": "archive", - "ext": ["zip"], - "mimeType": ["application/zip"], - "optional": ["xml", "sld"], - }, - { - "id": "xml", - "label": "XML Metadata File", - "format": "metadata", - "ext": ["xml"], - "mimeType": ["application/json"], - "needsFiles": ["shp", "prj", "dbf", "shx", "csv", "tiff", "zip", "sld"], - }, - { - "id": "sld", - "label": "Styled Layer Descriptor (SLD)", - "format": "metadata", - "ext": ["sld"], - "mimeType": ["application/json"], - "needsFiles": ["shp", "prj", "dbf", "shx", "csv", "tiff", "zip", "xml"], - }, -] INSTALLED_APPS += ( "dynamic_models", # "importer", diff --git a/geonode/storage/tests.py b/geonode/storage/tests.py index 43adc794ffa..9a4401c36d5 100644 --- a/geonode/storage/tests.py +++ b/geonode/storage/tests.py @@ -573,23 +573,6 @@ def test_zip_file_should_correctly_index_file_extensions(self): # extensions found more than once get indexed self.assertIsNotNone(_files.get("csv_file_1")) - @override_settings( - SUPPORTED_DATASET_FILE_TYPES=[ - {"id": "kmz", "label": "kmz", "format": "vector", "ext": ["kmz"]}, - {"id": "kml", "label": "kml", "format": "vector", "ext": ["kml"]}, - ] - ) - def test_zip_file_should_correctly_recognize_main_extension_with_kmz(self): - # reinitiate the storage manager with the zip file - storage_manager = self.sut( - remote_files={"base_file": os.path.join(f"{self.project_root}", "tests/data/Italy.kmz")} - ) - storage_manager.clone_remote_files() - - self.assertIsNotNone(storage_manager.data_retriever.temporary_folder) - _files = storage_manager.get_retrieved_paths() - self.assertTrue("doc.kml" in _files.get("base_file"), msg=f"files available: {_files}") - def test_zip_file_should_correctly_recognize_main_extension_with_shp(self): # zipping files storage_manager = self.sut(remote_files=self.local_files_paths) diff --git a/geonode/tests/test_utils.py b/geonode/tests/test_utils.py index e39600b60f2..20ff8b67f4c 100644 --- a/geonode/tests/test_utils.py +++ b/geonode/tests/test_utils.py @@ -18,7 +18,6 @@ ######################################################################### import copy from unittest import TestCase -from django.test import override_settings from unittest.mock import patch from datetime import datetime, timedelta @@ -32,8 +31,7 @@ from geonode.geoserver.helpers import set_attributes from geonode.tests.base import GeoNodeBaseTestSupport from geonode.br.management.commands.utils.utils import ignore_time -from geonode.utils import copy_tree, get_supported_datasets_file_types, bbox_to_wkt -from geonode import settings +from geonode.utils import copy_tree, bbox_to_wkt class TestCopyTree(GeoNodeBaseTestSupport): @@ -205,39 +203,6 @@ def setUp(self): }, ] - @override_settings( - ADDITIONAL_DATASET_FILE_TYPES=[ - {"id": "dummy_type", "label": "Dummy Type", "format": "dummy", "ext": ["dummy"]}, - ] - ) - def test_should_append_additional_type_if_config_is_provided(self): - prev_count = len(settings.SUPPORTED_DATASET_FILE_TYPES) - supported_types = get_supported_datasets_file_types() - supported_keys = [t.get("id") for t in supported_types] - self.assertIn("dummy_type", supported_keys) - self.assertEqual(len(supported_keys), prev_count + 1) - - @override_settings( - ADDITIONAL_DATASET_FILE_TYPES=[ - { - "id": "shp", - "label": "Replaced type", - "format": "vector", - "ext": ["shp"], - "requires": ["shp", "prj", "dbf", "shx"], - "optional": ["xml", "sld"], - }, - ] - ) - def test_should_replace_the_type_id_if_already_exists(self): - prev_count = len(settings.SUPPORTED_DATASET_FILE_TYPES) - supported_types = get_supported_datasets_file_types() - supported_keys = [t.get("id") for t in supported_types] - self.assertIn("shp", supported_keys) - self.assertEqual(len(supported_keys), prev_count) - shp_type = [t for t in supported_types if t["id"] == "shp"][0] - self.assertEqual(shp_type["label"], "Replaced type") - class TestRegionsCrossingDateLine(TestCase): def setUp(self): diff --git a/geonode/upload/api/serializer.py b/geonode/upload/api/serializer.py index 4657ddb9f3d..9d807c44bb1 100644 --- a/geonode/upload/api/serializer.py +++ b/geonode/upload/api/serializer.py @@ -34,7 +34,7 @@ class Meta: "sld_file", "store_spatial_files", "skip_existing_layers", - "source", + "action", ) base_file = serializers.FileField() @@ -42,7 +42,7 @@ class Meta: sld_file = serializers.FileField(required=False) store_spatial_files = serializers.BooleanField(required=False, default=True) skip_existing_layers = serializers.BooleanField(required=False, default=False) - source = serializers.CharField(required=False, default="upload") + action = serializers.CharField(required=True) class OverwriteImporterSerializer(ImporterSerializer): diff --git a/geonode/upload/api/tests.py b/geonode/upload/api/tests.py index 5d6aa237c6f..95f722715c9 100644 --- a/geonode/upload/api/tests.py +++ b/geonode/upload/api/tests.py @@ -62,9 +62,7 @@ def test_upload_method_not_allowed(self): def test_raise_exception_if_file_is_not_a_handled(self): self.client.force_login(get_user_model().objects.get(username="admin")) - payload = { - "base_file": SimpleUploadedFile(name="file.invalid", content=b"abc"), - } + payload = {"base_file": SimpleUploadedFile(name="file.invalid", content=b"abc"), "action": "upload"} response = self.client.post(self.url, data=payload) self.assertEqual(500, response.status_code) @@ -76,6 +74,7 @@ def test_gpkg_raise_error_with_invalid_payload(self): content=b'{"type": "FeatureCollection", "content": "some-content"}', ), "store_spatial_files": "invalid", + "action": "upload", } expected = { "success": False, @@ -99,6 +98,7 @@ def test_gpkg_task_is_called(self, patch_upload): content=b'{"type": "FeatureCollection", "content": "some-content"}', ), "store_spatial_files": True, + "action": "upload", } response = self.client.post(self.url, data=payload) @@ -116,6 +116,7 @@ def test_geojson_task_is_called(self, patch_upload): content=b'{"type": "FeatureCollection", "content": "some-content"}', ), "store_spatial_files": True, + "action": "upload", } response = self.client.post(self.url, data=payload) @@ -133,6 +134,7 @@ def test_zip_file_is_unzip_and_the_handler_is_found(self, patch_upload): "base_file": open(f"{project_dir}/tests/fixture/valid.zip", "rb"), "zip_file": open(f"{project_dir}/tests/fixture/valid.zip", "rb"), "store_spatial_files": True, + "action": "upload", } response = self.client.post(self.url, data=payload) @@ -191,6 +193,7 @@ def test_asset_is_created_before_the_import_start(self, patch_upload): content=b'{"type": "FeatureCollection", "content": "some-content"}', ), "store_spatial_files": True, + "action": "upload", } response = self.client.post(self.url, data=payload) @@ -221,6 +224,7 @@ def test_asset_should_be_deleted_if_created_during_with_exception( content=b'{"type": "FeatureCollection", "content": "some-content"}', ), "store_spatial_files": True, + "action": "upload", } response = self.client.post(self.url, data=payload) diff --git a/geonode/upload/api/views.py b/geonode/upload/api/views.py index 53f86bab425..67fafe6a300 100644 --- a/geonode/upload/api/views.py +++ b/geonode/upload/api/views.py @@ -176,7 +176,6 @@ def create(self, request, *args, **kwargs): ) handler = orchestrator.get_handler(_data) - # not file but handler means that is a remote resource if handler: asset = None @@ -191,8 +190,6 @@ def create(self, request, *args, **kwargs): self.validate_upload(request, storage_manager) - action = ExecutionRequestAction.IMPORT.value - input_params = { **{"files": files, "handler_module_path": str(handler)}, **extracted_params, @@ -205,7 +202,7 @@ def create(self, request, *args, **kwargs): "asset_module_path": f"{asset.__module__}.{asset.__class__.__name__}", } ) - + action = input_params.get("action") execution_id = orchestrator.create_execution_request( user=request.user, func_name=next(iter(handler.get_task_list(action=action))), @@ -213,7 +210,6 @@ def create(self, request, *args, **kwargs): input_params=input_params, action=action, name=_file.name if _file else extracted_params.get("title", None), - source=extracted_params.get("source"), ) sig = import_orchestrator.s(files, str(execution_id), handler=str(handler), action=action) @@ -234,7 +230,7 @@ def create(self, request, *args, **kwargs): logger.exception(e) raise ImportException(detail=e.args[0] if len(e.args) > 0 else e) - raise ImportException(detail="No handlers found for this dataset type") + raise ImportException(detail="No handlers found for this dataset type/action") def _handle_asset(self, request, asset_dir, storage_manager, _data, handler): if storage_manager is None: @@ -328,7 +324,6 @@ def copy(self, request, *args, **kwargs): **{"handler_module_path": handler_module_path}, **extracted_params, }, - source="importer_copy", ) sig = import_orchestrator.s( diff --git a/geonode/upload/celery_tasks.py b/geonode/upload/celery_tasks.py index 6a270d70133..2aeb9170cf2 100644 --- a/geonode/upload/celery_tasks.py +++ b/geonode/upload/celery_tasks.py @@ -90,7 +90,7 @@ def import_orchestrator( step="start_import", layer_name=None, alternate=None, - action=exa.IMPORT.value, + action=exa.UPLOAD.value, **kwargs, ): """ @@ -179,7 +179,7 @@ def import_resource(self, execution_id, /, handler_module_path, action, **kwargs call_rollback_function( execution_id, handlers_module_path=handler_module_path, - prev_action=exa.IMPORT.value, + prev_action=exa.UPLOAD.value, layer=None, alternate=None, error=e, @@ -309,7 +309,7 @@ def create_geonode_resource( layer_name: Optional[str] = None, alternate: Optional[str] = None, handler_module_path: str = None, - action: str = exa.IMPORT.value, + action: str = exa.UPLOAD.value, **kwargs, ): """ diff --git a/geonode/upload/handlers/README.md b/geonode/upload/handlers/README.md index 255c2b08331..1c85117d55e 100644 --- a/geonode/upload/handlers/README.md +++ b/geonode/upload/handlers/README.md @@ -32,7 +32,7 @@ class BaseVectorFileHandler(BaseHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { + TASKS = { exa.IMPORT.value: (), # define the list of the step (celery task) needed to execute the action for the resource exa.COPY.value: (), exa.DELETE.value: (), @@ -242,7 +242,7 @@ class NewVectorFileHandler(BaseVectorFileHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { + TASKS = { exa.IMPORT.value: ( "start_import", "geonode.upload.import_resource", diff --git a/geonode/upload/handlers/apps.py b/geonode/upload/handlers/apps.py index 7e50e59aae5..72e26b7c585 100644 --- a/geonode/upload/handlers/apps.py +++ b/geonode/upload/handlers/apps.py @@ -41,21 +41,3 @@ def run_setup_hooks(*args, **kwargs): for item in _handlers: item.register() logger.info(f"The following handlers have been registered: {', '.join(available_handlers)}") - - _available_settings = [ - import_string(module_path)().supported_file_extension_config - for module_path in available_handlers - if import_string(module_path)().supported_file_extension_config - ] - # injecting the new config required for FE - supported_type = [] - supported_type.extend(_available_settings) - if not getattr(settings, "ADDITIONAL_DATASET_FILE_TYPES", None): - setattr(settings, "ADDITIONAL_DATASET_FILE_TYPES", supported_type) - elif "gpkg" not in [x.get("id") for x in settings.ADDITIONAL_DATASET_FILE_TYPES]: - settings.ADDITIONAL_DATASET_FILE_TYPES.extend(supported_type) - setattr( - settings, - "ADDITIONAL_DATASET_FILE_TYPES", - settings.ADDITIONAL_DATASET_FILE_TYPES, - ) diff --git a/geonode/upload/handlers/base.py b/geonode/upload/handlers/base.py index 5afb57bbe76..1c2407bf6c1 100644 --- a/geonode/upload/handlers/base.py +++ b/geonode/upload/handlers/base.py @@ -46,8 +46,8 @@ class BaseHandler(ABC): REGISTRY = [] - ACTIONS = { - exa.IMPORT.value: (), + TASKS = { + exa.UPLOAD.value: (), exa.COPY.value: (), exa.DELETE.value: (), exa.UPDATE.value: (), @@ -70,9 +70,9 @@ def get_registry(cls): @classmethod def get_task_list(cls, action) -> tuple: - if action not in cls.ACTIONS: + if action not in cls.TASKS: raise Exception("The requested action is not implemented yet") - return cls.ACTIONS.get(action) + return cls.TASKS.get(action) @property def default_geometry_column_name(self): @@ -140,7 +140,7 @@ def can_do(action) -> bool: the Handler must be ready to handle them. If is not in the actual flow the already in place flow is followd """ - return action in BaseHandler.ACTIONS + return action in BaseHandler.TASKS @staticmethod def extract_params_from_data(_data): @@ -300,7 +300,7 @@ def overwrite_resourcehandlerinfo( return self.create_resourcehandlerinfo(handler_module_path, resource, execution_id, **kwargs) def rollback(self, exec_id, rollback_from_step, action_to_rollback, *args, **kwargs): - steps = self.ACTIONS.get(action_to_rollback) + steps = self.TASKS.get(action_to_rollback) if rollback_from_step not in steps: logger.info(f"Step not found {rollback_from_step}, skipping") diff --git a/geonode/upload/handlers/common/metadata.py b/geonode/upload/handlers/common/metadata.py index 35374a42a59..06ab95acc1c 100644 --- a/geonode/upload/handlers/common/metadata.py +++ b/geonode/upload/handlers/common/metadata.py @@ -17,12 +17,9 @@ # ######################################################################### import logging -from geonode.resource.enumerator import ExecutionRequestAction as exa from geonode.upload.handlers.base import BaseHandler -from geonode.upload.handlers.utils import UploadSourcesEnum from geonode.upload.models import ResourceHandlerInfo from geonode.upload.handlers.xml.serializer import MetadataFileSerializer -from geonode.upload.utils import ImporterRequestAction as ira from geonode.upload.orchestrator import orchestrator from django.shortcuts import get_object_or_404 from geonode.layers.models import Dataset @@ -36,24 +33,6 @@ class MetadataFileHandler(BaseHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { - exa.IMPORT.value: ("start_import", "geonode.upload.import_resource"), - ira.ROLLBACK.value: ( - "start_rollback", - "geonode.upload.rollback", - ), - } - - @staticmethod - def can_handle(_data) -> bool: - """ - This endpoint will return True or False if with the info provided - the handler is able to handle the file or not - """ - if _data.get("source", None) == UploadSourcesEnum.resource_file_upload.value: - return True - return False - @staticmethod def has_serializer(data) -> bool: _base = data.get("base_file") @@ -79,7 +58,7 @@ def extract_params_from_data(_data, action=None): "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), "resource_pk": _data.pop("resource_pk", None), "store_spatial_file": _data.pop("store_spatial_files", "True"), - "source": _data.pop("source", "resource_file_upload"), + "action": _data.pop("action"), }, _data @staticmethod diff --git a/geonode/upload/handlers/common/raster.py b/geonode/upload/handlers/common/raster.py index e581d739d1f..fd8e59b54ff 100644 --- a/geonode/upload/handlers/common/raster.py +++ b/geonode/upload/handlers/common/raster.py @@ -35,7 +35,7 @@ from geonode.upload.celery_tasks import ErrorBaseTaskClass, import_orchestrator from geonode.upload.handlers.base import BaseHandler from geonode.upload.handlers.geotiff.exceptions import InvalidGeoTiffException -from geonode.upload.handlers.utils import UploadSourcesEnum, create_alternate, should_be_imported +from geonode.upload.handlers.utils import create_alternate, should_be_imported from geonode.upload.models import ResourceHandlerInfo from geonode.upload.orchestrator import orchestrator from osgeo import gdal @@ -83,16 +83,6 @@ def is_valid(files, user, **kwargs): raise ImportException(stderr) return True - @staticmethod - def can_handle(_data) -> bool: - """ - This endpoint will return True or False if with the info provided - the handler is able to handle the file or not - """ - if _data.get("source", None) != UploadSourcesEnum.upload.value: - return False - return True - @staticmethod def has_serializer(_data) -> bool: """ @@ -107,7 +97,7 @@ def can_do(action) -> bool: This endpoint will return True or False if with the info provided the handler is able to handle the file or not """ - return action in BaseHandler.ACTIONS + return action in BaseHandler.TASKS @staticmethod def create_error_log(exc, task_name, *args): @@ -132,7 +122,7 @@ def extract_params_from_data(_data, action=None): "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), "resource_pk": _data.pop("resource_pk", None), "store_spatial_file": _data.pop("store_spatial_files", "True"), - "source": _data.pop("source", "upload"), + "action": _data.pop("action", "upload"), }, _data @staticmethod @@ -285,6 +275,8 @@ def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: dataset = Dataset.objects.filter(pk=_exec.input_params.get("resource_pk")).first() if not dataset: raise ImportException("The dataset selected for the ovewrite does not exists") + if dataset.is_vector(): + raise Exception("cannot override a vector dataset with a raster one") alternate = dataset.alternate.split(":")[-1] orchestrator.update_execution_request_obj(_exec, {"geonode_resource": dataset}) else: @@ -293,6 +285,8 @@ def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: dataset_exists = user_datasets.exists() if dataset_exists and should_be_overwritten: + if user_datasets.is_vector(): + raise Exception("cannot override a vector dataset with a raster one") layer_name, alternate = ( layer_name, user_datasets.first().alternate.split(":")[-1], @@ -310,7 +304,7 @@ def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: "geonode.upload.import_resource", layer_name, alternate, - exa.IMPORT.value, + exa.UPLOAD.value, ) ) return layer_name, alternate, execution_id diff --git a/geonode/upload/handlers/common/remote.py b/geonode/upload/handlers/common/remote.py index f72579e2708..44105755902 100755 --- a/geonode/upload/handlers/common/remote.py +++ b/geonode/upload/handlers/common/remote.py @@ -47,8 +47,8 @@ class BaseRemoteResourceHandler(BaseHandler): As first implementation only remote 3dtiles are supported """ - ACTIONS = { - exa.IMPORT.value: ( + TASKS = { + exa.UPLOAD.value: ( "start_import", "geonode.upload.import_resource", "geonode.upload.create_geonode_resource", @@ -105,7 +105,7 @@ def extract_params_from_data(_data, action=None): return {"title": title.pop("title"), "store_spatial_file": True}, _data return { - "source": _data.pop("source", "upload"), + "action": _data.pop("action", "upload"), "title": _data.pop("title", None), "url": _data.pop("url", None), "type": _data.pop("type", None), @@ -163,7 +163,7 @@ def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: "geonode.upload.import_resource", layer_name, alternate, - exa.IMPORT.value, + exa.UPLOAD.value, ) ) return layer_name, alternate, execution_id diff --git a/geonode/upload/handlers/common/serializer.py b/geonode/upload/handlers/common/serializer.py index f1b12b7d8db..c1eab398781 100644 --- a/geonode/upload/handlers/common/serializer.py +++ b/geonode/upload/handlers/common/serializer.py @@ -26,7 +26,7 @@ class Meta: ref_name = "RemoteResourceSerializer" model = ResourceBase view_name = "importer_upload" - fields = ("url", "title", "type", "source", "overwrite_existing_layer") + fields = ("url", "title", "type", "action", "overwrite_existing_layer") url = serializers.URLField(required=True, help_text="URL of the remote service / resource") title = serializers.CharField(required=True, help_text="Title of the resource. Can be None or Empty") @@ -34,6 +34,6 @@ class Meta: required=True, help_text="Remote resource type, for example wms or 3dtiles. Is used by the handler to understand if can handle the resource", ) - source = serializers.CharField(required=False, default="upload") + action = serializers.CharField(required=True) overwrite_existing_layer = serializers.BooleanField(required=False, default=False) diff --git a/geonode/upload/handlers/common/test_remote.py b/geonode/upload/handlers/common/test_remote.py index 89430d618b9..c61b02981f1 100644 --- a/geonode/upload/handlers/common/test_remote.py +++ b/geonode/upload/handlers/common/test_remote.py @@ -82,16 +82,16 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.import_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 3) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 3) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_geojson(self): expected = ( "start_copy", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 2) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 2) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_url_is_invalid(self): with self.assertRaises(ImportException) as _exc: @@ -106,7 +106,7 @@ def test_is_valid_should_pass_with_valid_url(self): def test_extract_params_from_data(self): actual, _data = self.handler.extract_params_from_data( _data={"defaults": '{"url": "http://abc123defsadsa.org", "title": "Remote Title", "type": "3dtiles"}'}, - action="import", + action="upload", ) self.assertTrue("title" in actual) self.assertTrue("url" in actual) diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index 3aa5a48040c..2aed508cb82 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -39,7 +39,6 @@ from geonode.upload.handlers.utils import ( GEOM_TYPE_MAPPING, STANDARD_TYPE_MAPPING, - UploadSourcesEnum, drop_dynamic_model_schema, ) from geonode.resource.manager import resource_manager @@ -55,6 +54,7 @@ from django.db.models import Q import pyproj from geonode.geoserver.security import delete_dataset_cache, set_geowebcache_invalidate_cache +from geonode.upload.utils import ImporterRequestAction as ira logger = logging.getLogger("importer") @@ -65,6 +65,32 @@ class BaseVectorFileHandler(BaseHandler): It must provide the task_lists required to comple the upload """ + TASKS = { + exa.UPLOAD.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + exa.COPY.value: ( + "start_copy", + "geonode.upload.copy_dynamic_model", + "geonode.upload.copy_geonode_data_table", + "geonode.upload.publish_resource", + "geonode.upload.copy_geonode_resource", + ), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + ira.REPLACE.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), + } + @property def default_geometry_column_name(self): return "geometry" @@ -99,7 +125,7 @@ def can_handle(_data) -> bool: This endpoint will return True or False if with the info provided the handler is able to handle the file or not """ - if _data.get("source", None) != UploadSourcesEnum.upload.value: + if _data.get("action", None) not in BaseVectorFileHandler.TASKS: return False return True @@ -117,7 +143,7 @@ def can_do(action) -> bool: This endpoint will return True or False if with the info provided the handler is able to handle the file or not """ - return action in BaseHandler.ACTIONS + return action in BaseHandler.TASKS @staticmethod def create_error_log(exc, task_name, *args): @@ -142,7 +168,7 @@ def extract_params_from_data(_data, action=None): "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), "resource_pk": _data.pop("resource_pk", None), "store_spatial_file": _data.pop("store_spatial_files", "True"), - "source": _data.pop("source", "upload"), + "action": _data.pop("action", "upload"), }, _data @staticmethod @@ -431,6 +457,9 @@ def find_alternate_by_dataset(self, _exec_obj, layer_name, should_be_overwritten dataset = Dataset.objects.filter(pk=_exec_obj.input_params.get("resource_pk")).first() if not dataset: raise ImportException("The dataset selected for the ovewrite does not exists") + if should_be_overwritten: + if not dataset.is_vector(): + raise Exception("Cannot override a raster dataset with a vector one") alternate = dataset.alternate.split(":") return alternate[-1] @@ -438,6 +467,9 @@ def find_alternate_by_dataset(self, _exec_obj, layer_name, should_be_overwritten dataset_available = Dataset.objects.filter(alternate__iexact=f"{workspace.name}:{layer_name}") dataset_exists = dataset_available.exists() + if should_be_overwritten: + if not dataset_available.is_vector(): + raise Exception("Cannot override a raster dataset with a vector one") if dataset_exists and should_be_overwritten: alternate = dataset_available.first().alternate.split(":")[-1] @@ -851,7 +883,7 @@ def import_next_step( actual_step, layer_name, alternate, - exa.IMPORT.value, + exa.UPLOAD.value, ) import_orchestrator.apply_async(task_params, kwargs) @@ -859,7 +891,7 @@ def import_next_step( call_rollback_function( execution_id, handlers_module_path=handlers_module_path, - prev_action=exa.IMPORT.value, + prev_action=exa.UPLOAD.value, layer=layer_name, alternate=alternate, error=e, @@ -927,7 +959,7 @@ def import_with_ogr2ogr( call_rollback_function( execution_id, handlers_module_path=handler_module_path, - prev_action=exa.IMPORT.value, + prev_action=exa.UPLOAD.value, layer=original_name, alternate=alternate, error=e, diff --git a/geonode/upload/handlers/csv/handler.py b/geonode/upload/handlers/csv/handler.py index 1e3483a25a8..3d26a4d6e66 100644 --- a/geonode/upload/handlers/csv/handler.py +++ b/geonode/upload/handlers/csv/handler.py @@ -29,7 +29,6 @@ from dynamic_models.models import ModelSchema from geonode.upload.handlers.common.vector import BaseVectorFileHandler from geonode.upload.handlers.utils import GEOM_TYPE_MAPPING -from geonode.upload.utils import ImporterRequestAction as ira logger = logging.getLogger("importer") @@ -40,26 +39,6 @@ class CSVFileHandler(BaseVectorFileHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { - exa.IMPORT.value: ( - "start_import", - "geonode.upload.import_resource", - "geonode.upload.publish_resource", - "geonode.upload.create_geonode_resource", - ), - exa.COPY.value: ( - "start_copy", - "geonode.upload.copy_dynamic_model", - "geonode.upload.copy_geonode_data_table", - "geonode.upload.publish_resource", - "geonode.upload.copy_geonode_resource", - ), - ira.ROLLBACK.value: ( - "start_rollback", - "geonode.upload.rollback", - ), - } - possible_geometry_column_name = ["geom", "geometry", "wkt_geom", "the_geom"] possible_lat_column = ["latitude", "lat", "y"] possible_long_column = ["longitude", "long", "x"] @@ -69,11 +48,15 @@ class CSVFileHandler(BaseVectorFileHandler): def supported_file_extension_config(self): return { "id": "csv", - "label": "CSV", - "format": "vector", - "mimeType": ["text/csv"], - "ext": ["csv"], - "optional": ["sld", "xml"], + "formats": [ + { + "label": "CSV", + "required_ext": ["csv"], + "optional_ext": ["sld", "xml"], + } + ], + "actions": list(self.TASKS.keys()), + "type": "vector", } @staticmethod diff --git a/geonode/upload/handlers/csv/tests.py b/geonode/upload/handlers/csv/tests.py index dc75996bef4..6321b880a88 100644 --- a/geonode/upload/handlers/csv/tests.py +++ b/geonode/upload/handlers/csv/tests.py @@ -46,7 +46,7 @@ def setUpClass(cls): cls.missing_geom = f"{project_dir}/tests/fixture/missing_geom.csv" cls.user, _ = get_user_model().objects.get_or_create(username="admin") cls.invalid_files = {"base_file": cls.invalid_csv} - cls.valid_files = {"base_file": cls.valid_csv, "source": "upload"} + cls.valid_files = {"base_file": cls.valid_csv, "action": "upload"} cls.owner = get_user_model().objects.first() cls.layer = create_single_dataset(name="test", owner=cls.owner) @@ -57,8 +57,8 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.publish_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 4) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 4) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_geojson(self): expected = ( @@ -68,8 +68,8 @@ def test_task_list_is_the_expected_one_geojson(self): "geonode.upload.publish_resource", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_csv_is_invalid(self): with self.assertRaises(InvalidCSVException) as _exc: diff --git a/geonode/upload/handlers/geojson/handler.py b/geonode/upload/handlers/geojson/handler.py index 6060be709bf..eb4c740463d 100644 --- a/geonode/upload/handlers/geojson/handler.py +++ b/geonode/upload/handlers/geojson/handler.py @@ -19,11 +19,9 @@ import json import logging import os -from geonode.resource.enumerator import ExecutionRequestAction as exa from geonode.upload.utils import UploadLimitValidator from geonode.upload.handlers.common.vector import BaseVectorFileHandler from osgeo import ogr -from geonode.upload.utils import ImporterRequestAction as ira from geonode.upload.handlers.geojson.exceptions import InvalidGeoJsonException @@ -36,34 +34,24 @@ class GeoJsonFileHandler(BaseVectorFileHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { - exa.IMPORT.value: ( - "start_import", - "geonode.upload.import_resource", - "geonode.upload.publish_resource", - "geonode.upload.create_geonode_resource", - ), - exa.COPY.value: ( - "start_copy", - "geonode.upload.copy_dynamic_model", - "geonode.upload.copy_geonode_data_table", - "geonode.upload.publish_resource", - "geonode.upload.copy_geonode_resource", - ), - ira.ROLLBACK.value: ( - "start_rollback", - "geonode.upload.rollback", - ), - } - @property def supported_file_extension_config(self): return { "id": "geojson", - "label": "GeoJSON", - "format": "vector", - "ext": ["json", "geojson"], - "optional": ["xml", "sld"], + "formats": [ + { + "label": "GeoJSON", + "required_ext": ["geojson"], + "optional_ext": ["sld", "xml"], + }, + { + "label": "GeoJSON", + "required_ext": ["json"], + "optional_ext": ["sld", "xml"], + }, + ], + "actions": list(self.TASKS.keys()), + "type": "vector", } @staticmethod diff --git a/geonode/upload/handlers/geojson/tests.py b/geonode/upload/handlers/geojson/tests.py index acf5913e74e..ba222c2dec3 100644 --- a/geonode/upload/handlers/geojson/tests.py +++ b/geonode/upload/handlers/geojson/tests.py @@ -43,7 +43,7 @@ def setUpClass(cls): cls.invalid_geojson = f"{project_dir}/tests/fixture/invalid.geojson" cls.user, _ = get_user_model().objects.get_or_create(username="admin") cls.invalid_files = {"base_file": cls.invalid_geojson} - cls.valid_files = {"base_file": cls.valid_geojson, "source": "upload"} + cls.valid_files = {"base_file": cls.valid_geojson, "action": "upload"} cls.owner = get_user_model().objects.first() cls.layer = create_single_dataset(name="stazioni_metropolitana", owner=cls.owner) @@ -54,8 +54,8 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.publish_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 4) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 4) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_copy(self): expected = ( @@ -65,8 +65,8 @@ def test_task_list_is_the_expected_one_copy(self): "geonode.upload.publish_resource", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") diff --git a/geonode/upload/handlers/geotiff/handler.py b/geonode/upload/handlers/geotiff/handler.py index e49584a9639..232440360df 100644 --- a/geonode/upload/handlers/geotiff/handler.py +++ b/geonode/upload/handlers/geotiff/handler.py @@ -34,8 +34,8 @@ class GeoTiffFileHandler(BaseRasterFileHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { - exa.IMPORT.value: ( + TASKS = { + exa.UPLOAD.value: ( "start_import", "geonode.upload.import_resource", "geonode.upload.publish_resource", @@ -51,17 +51,42 @@ class GeoTiffFileHandler(BaseRasterFileHandler): "start_rollback", "geonode.upload.rollback", ), + ira.REPLACE.value: ( + "start_import", + "geonode.upload.import_resource", + "geonode.upload.publish_resource", + "geonode.upload.create_geonode_resource", + ), } @property def supported_file_extension_config(self): return { "id": "tiff", - "label": "GeoTIFF", - "format": "raster", - "ext": ["tiff", "tif", "geotiff", "geotif"], - "mimeType": ["image/tiff"], - "optional": ["xml", "sld"], + "formats": [ + { + "label": "TIFF", + "required_ext": ["tiff"], + "optional_ext": ["xml", "sld"], + }, + { + "label": "TIF", + "required_ext": ["tif"], + "optional_ext": ["xml", "sld"], + }, + { + "label": "GeoTIFF", + "required_ext": ["geotiff"], + "optional_ext": ["xml", "sld"], + }, + { + "label": "GeoTIF", + "required_ext": ["geotif"], + "optional_ext": ["xml", "sld"], + }, + ], + "actions": list(self.TASKS.keys()), + "type": "raster", } @staticmethod @@ -74,7 +99,7 @@ def can_handle(_data) -> bool: if not base: return False ext = base.split(".")[-1] if isinstance(base, str) else base.name.split(".")[-1] - return ext in ["tiff", "geotiff", "tif", "geotif"] and BaseRasterFileHandler.can_handle(_data) + return ext in ["tiff", "geotiff", "tif", "geotif"] and _data.get("action", None) in GeoTiffFileHandler.TASKS @staticmethod def is_valid(files, user, **kwargs): diff --git a/geonode/upload/handlers/geotiff/tests.py b/geonode/upload/handlers/geotiff/tests.py index 882236409a9..be18b958896 100644 --- a/geonode/upload/handlers/geotiff/tests.py +++ b/geonode/upload/handlers/geotiff/tests.py @@ -35,7 +35,7 @@ def setUpClass(cls): super().setUpClass() cls.handler = GeoTiffFileHandler() cls.valid_tiff = f"{project_dir}/tests/fixture/test_raster.tif" - cls.valid_files = {"base_file": cls.valid_tiff, "source": "upload"} + cls.valid_files = {"base_file": cls.valid_tiff, "action": "upload"} cls.user, _ = get_user_model().objects.get_or_create(username="admin") cls.invalid_tiff = {"base_file": "invalid.file.foo"} cls.owner = get_user_model().objects.first() @@ -48,8 +48,8 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.publish_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 4) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 4) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_copy(self): expected = ( @@ -58,8 +58,8 @@ def test_task_list_is_the_expected_one_copy(self): "geonode.upload.publish_resource", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 4) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 4) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") diff --git a/geonode/upload/handlers/gpkg/handler.py b/geonode/upload/handlers/gpkg/handler.py index 1742d12c9e1..59553de453a 100644 --- a/geonode/upload/handlers/gpkg/handler.py +++ b/geonode/upload/handlers/gpkg/handler.py @@ -37,8 +37,8 @@ class GPKGFileHandler(BaseVectorFileHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { - exa.IMPORT.value: ( + TASKS = { + exa.UPLOAD.value: ( "start_import", "geonode.upload.import_resource", "geonode.upload.publish_resource", @@ -61,9 +61,14 @@ class GPKGFileHandler(BaseVectorFileHandler): def supported_file_extension_config(self): return { "id": "gpkg", - "label": "GeoPackage", - "format": "vector", - "ext": ["gpkg"], + "formats": [ + { + "label": "GeoPackage", + "required_ext": ["gpkg"], + } + ], + "actions": list(self.TASKS.keys()), + "type": "vector", } @property @@ -84,9 +89,9 @@ def can_handle(_data) -> bool: base = _data.get("base_file") if not base: return False - return ( - base.endswith(".gpkg") if isinstance(base, str) else base.name.endswith(".gpkg") - ) and BaseVectorFileHandler.can_handle(_data) + return (base.endswith(".gpkg") if isinstance(base, str) else base.name.endswith(".gpkg")) and _data.get( + "action", None + ) in GPKGFileHandler.TASKS @staticmethod def is_valid(files, user, **kwargs): diff --git a/geonode/upload/handlers/gpkg/tests.py b/geonode/upload/handlers/gpkg/tests.py index 32770d5c393..8ea31059548 100644 --- a/geonode/upload/handlers/gpkg/tests.py +++ b/geonode/upload/handlers/gpkg/tests.py @@ -44,7 +44,7 @@ def setUpClass(cls): cls.invalid_gpkg = f"{project_dir}/tests/fixture/invalid.gpkg" cls.user, _ = get_user_model().objects.get_or_create(username="admin") cls.invalid_files = {"base_file": cls.invalid_gpkg} - cls.valid_files = {"base_file": cls.valid_gpkg, "source": "upload"} + cls.valid_files = {"base_file": cls.valid_gpkg, "action": "upload"} cls.owner = get_user_model().objects.first() cls.layer = create_single_dataset(name="stazioni_metropolitana", owner=cls.owner) @@ -55,8 +55,8 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.publish_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 4) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 4) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_geojson(self): expected = ( @@ -66,8 +66,8 @@ def test_task_list_is_the_expected_one_geojson(self): "geonode.upload.publish_resource", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_gpkg_is_invalid(self): with self.assertRaises(InvalidGeopackageException) as _exc: diff --git a/geonode/upload/handlers/kml/handler.py b/geonode/upload/handlers/kml/handler.py index 8ab7f9d00a4..aaaa07cb4c5 100644 --- a/geonode/upload/handlers/kml/handler.py +++ b/geonode/upload/handlers/kml/handler.py @@ -37,8 +37,8 @@ class KMLFileHandler(BaseVectorFileHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { - exa.IMPORT.value: ( + TASKS = { + exa.UPLOAD.value: ( "start_import", "geonode.upload.import_resource", "geonode.upload.publish_resource", @@ -61,9 +61,15 @@ class KMLFileHandler(BaseVectorFileHandler): def supported_file_extension_config(self): return { "id": "kml", - "label": "KML/KMZ", - "format": "vector", - "ext": ["kml", "kmz"], + "formats": [ + {"label": "KML", "required_ext": ["kml"]}, + { + "label": "KMZ", + "required_ext": ["kmz"], + }, + ], + "actions": list(self.TASKS.keys()), + "type": "vector", } @property diff --git a/geonode/upload/handlers/kml/tests.py b/geonode/upload/handlers/kml/tests.py index 9fe0a873c47..2b03a0a0a06 100644 --- a/geonode/upload/handlers/kml/tests.py +++ b/geonode/upload/handlers/kml/tests.py @@ -38,7 +38,7 @@ def setUpClass(cls): cls.invalid_kml = f"{project_dir}/tests/fixture/inva.lid.kml" cls.user, _ = get_user_model().objects.get_or_create(username="admin") cls.invalid_files = {"base_file": cls.invalid_kml} - cls.valid_files = {"base_file": cls.valid_kml, "source": "upload"} + cls.valid_files = {"base_file": cls.valid_kml, "action": "upload"} cls.owner = get_user_model().objects.first() cls.layer = create_single_dataset(name="extruded_polygon", owner=cls.owner) @@ -49,8 +49,8 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.publish_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 4) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 4) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_geojson(self): expected = ( @@ -60,8 +60,8 @@ def test_task_list_is_the_expected_one_geojson(self): "geonode.upload.publish_resource", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_kml_is_invalid(self): with self.assertRaises(InvalidKmlException) as _exc: diff --git a/geonode/upload/handlers/remote/tests/test_3dtiles.py b/geonode/upload/handlers/remote/tests/test_3dtiles.py index 675b65e92f4..f5216ec9414 100644 --- a/geonode/upload/handlers/remote/tests/test_3dtiles.py +++ b/geonode/upload/handlers/remote/tests/test_3dtiles.py @@ -83,16 +83,16 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.import_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 3) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 3) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_geojson(self): expected = ( "start_copy", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 2) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 2) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_url_is_invalid(self): with self.assertRaises(ImportException) as _exc: @@ -107,7 +107,7 @@ def test_is_valid_should_pass_with_valid_url(self): def test_extract_params_from_data(self): actual, _data = self.handler.extract_params_from_data( _data={"defaults": '{"url": "http://abc123defsadsa.org", "title": "Remote Title", "type": "3dtiles"}'}, - action="import", + action="upload", ) self.assertTrue("title" in actual) self.assertTrue("url" in actual) diff --git a/geonode/upload/handlers/remote/tests/test_wms.py b/geonode/upload/handlers/remote/tests/test_wms.py index 5330343b405..1bb00c1a71a 100644 --- a/geonode/upload/handlers/remote/tests/test_wms.py +++ b/geonode/upload/handlers/remote/tests/test_wms.py @@ -35,9 +35,7 @@ class TestRemoteWMSResourceHandler(TestCase): def setUpClass(cls): super().setUpClass() cls.handler = RemoteWMSResourceHandler() - cls.valid_url = ( - "https://development.demo.geonode.org/geoserver/ows?service=WMS&version=1.3.0&request=GetCapabilities" - ) + cls.valid_url = "http://geoserver:8080/geoserver/ows?service=WMS&version=1.3.0&request=GetCapabilities" cls.user, _ = get_user_model().objects.get_or_create(username="admin") cls.invalid_payload = { "url": "http://invalid.com", @@ -96,16 +94,16 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.import_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 3) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 3) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_geojson(self): expected = ( "start_copy", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 2) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 2) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_url_is_invalid(self): with self.assertRaises(ImportException) as _exc: @@ -120,7 +118,7 @@ def test_is_valid_should_pass_with_valid_url(self): def test_extract_params_from_data(self): actual, _data = self.handler.extract_params_from_data( _data={"defaults": f"{self.valid_payload_with_parse_true}"}, - action="import", + action="upload", ) self.assertTrue("title" in actual) self.assertTrue("url" in actual) diff --git a/geonode/upload/handlers/remote/tiles3d.py b/geonode/upload/handlers/remote/tiles3d.py index 94f04199abc..c47df255e2a 100644 --- a/geonode/upload/handlers/remote/tiles3d.py +++ b/geonode/upload/handlers/remote/tiles3d.py @@ -33,6 +33,10 @@ class RemoteTiles3DResourceHandler(BaseRemoteResourceHandler, Tiles3DFileHandler): + @property + def supported_file_extension_config(self): + return {} + @staticmethod def has_serializer(data) -> bool: if "url" in data and "3dtiles" in data.get("type", "").lower(): diff --git a/geonode/upload/handlers/shapefile/handler.py b/geonode/upload/handlers/shapefile/handler.py index 1f5951c32b8..ff7033c9840 100644 --- a/geonode/upload/handlers/shapefile/handler.py +++ b/geonode/upload/handlers/shapefile/handler.py @@ -29,7 +29,6 @@ from geonode.upload.handlers.shapefile.exceptions import InvalidShapeFileException from geonode.upload.handlers.shapefile.serializer import OverwriteShapeFileSerializer, ShapeFileSerializer -from geonode.upload.utils import ImporterRequestAction as ira logger = logging.getLogger("importer") @@ -40,35 +39,19 @@ class ShapeFileHandler(BaseVectorFileHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { - exa.IMPORT.value: ( - "start_import", - "geonode.upload.import_resource", - "geonode.upload.publish_resource", - "geonode.upload.create_geonode_resource", - ), - exa.COPY.value: ( - "start_copy", - "geonode.upload.copy_dynamic_model", - "geonode.upload.copy_geonode_data_table", - "geonode.upload.publish_resource", - "geonode.upload.copy_geonode_resource", - ), - ira.ROLLBACK.value: ( - "start_rollback", - "geonode.upload.rollback", - ), - } - @property def supported_file_extension_config(self): return { "id": "shp", - "label": "ESRI Shapefile", - "format": "vector", - "ext": ["shp"], - "requires": ["shp", "prj", "dbf", "shx"], - "optional": ["xml", "sld", "cpg", "cst"], + "formats": [ + { + "label": "ESRI Shapefile", + "required_ext": ["shp", "prj", "dbf", "shx"], + "optional_ext": ["xml", "sld", "cpg", "cst"], + } + ], + "actions": list(self.TASKS.keys()), + "type": "vector", } @staticmethod @@ -110,7 +93,7 @@ def extract_params_from_data(_data, action=None): "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), "resource_pk": _data.pop("resource_pk", None), "store_spatial_file": _data.pop("store_spatial_files", "True"), - "source": _data.pop("source", "upload"), + "action": _data.pop("action", "upload"), } return additional_params, _data @@ -130,7 +113,7 @@ def is_valid(files, user, **kwargs): _filename = Path(_file).stem - _shp_ext_needed = [x["requires"] for x in get_supported_datasets_file_types() if x["id"] == "shp"][0] + _shp_ext_needed = ShapeFileHandler._get_ext_needed() """ Check if the ext required for the shape file are available in the files uploaded @@ -156,6 +139,13 @@ def is_valid(files, user, **kwargs): return True + @staticmethod + def _get_ext_needed(): + for x in get_supported_datasets_file_types(): + if x["id"] == "shp": + for item in x["formats"][0]["required_ext"]: + yield item + def get_ogr2ogr_driver(self): return ogr.GetDriverByName("ESRI Shapefile") diff --git a/geonode/upload/handlers/shapefile/serializer.py b/geonode/upload/handlers/shapefile/serializer.py index cf7aa407436..4091cddb5ac 100644 --- a/geonode/upload/handlers/shapefile/serializer.py +++ b/geonode/upload/handlers/shapefile/serializer.py @@ -36,7 +36,7 @@ class Meta: "store_spatial_files", "overwrite_existing_layer", "skip_existing_layers", - "source", + "action", ) base_file = serializers.FileField() @@ -48,7 +48,7 @@ class Meta: store_spatial_files = serializers.BooleanField(required=False, default=True) overwrite_existing_layer = serializers.BooleanField(required=False, default=False) skip_existing_layers = serializers.BooleanField(required=False, default=False) - source = serializers.CharField(required=False, default="upload") + action = serializers.CharField(required=True) class OverwriteShapeFileSerializer(ShapeFileSerializer): diff --git a/geonode/upload/handlers/shapefile/tests.py b/geonode/upload/handlers/shapefile/tests.py index f643306bf7e..d90c0fc06e8 100644 --- a/geonode/upload/handlers/shapefile/tests.py +++ b/geonode/upload/handlers/shapefile/tests.py @@ -48,7 +48,7 @@ def setUpClass(cls): "dbf_file": f"{file_path}/san_andres_y_providencia_highway.dbf", "prj_file": f"{file_path}/san_andres_y_providencia_highway.prj", "shx_file": f"{file_path}/san_andres_y_providencia_highway.shx", - "source": "upload", + "action": "upload", } cls.invalid_shp = f"{project_dir}/tests/fixture/invalid.geojson" cls.user, _ = get_user_model().objects.get_or_create(username="admin") @@ -62,8 +62,8 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.publish_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 4) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 4) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_copy_task_list_is_the_expected_one(self): expected = ( @@ -73,8 +73,8 @@ def test_copy_task_list_is_the_expected_one(self): "geonode.upload.publish_resource", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 5) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 5) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") diff --git a/geonode/upload/handlers/sld/handler.py b/geonode/upload/handlers/sld/handler.py index c6cd25863a4..14c5efb736f 100644 --- a/geonode/upload/handlers/sld/handler.py +++ b/geonode/upload/handlers/sld/handler.py @@ -22,6 +22,7 @@ from geonode.upload.handlers.common.metadata import MetadataFileHandler from geonode.upload.handlers.sld.exceptions import InvalidSldException from owslib.etree import etree as dlxml +from geonode.upload.utils import ImporterRequestAction as ira logger = logging.getLogger("importer") @@ -32,25 +33,26 @@ class SLDFileHandler(MetadataFileHandler): It must provide the task_lists required to comple the upload """ + TASKS = { + ira.RESOURCE_STYLE_UPLOAD.value: ("start_import", "geonode.upload.import_resource"), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + @property def supported_file_extension_config(self): return { "id": "sld", - "label": "Styled Layer Descriptor (SLD)", - "format": "metadata", - "ext": ["sld"], - "mimeType": ["application/json"], - "needsFiles": [ - "shp", - "prj", - "dbf", - "shx", - "csv", - "tiff", - "zip", - "xml", - "geojson", + "formats": [ + { + "label": "Styled Layer Descriptor 1.0, 1.1 (SLD)", + "required_ext": ["sld"], + } ], + "actions": list(self.TASKS.keys()), + "type": "metadata", } @staticmethod @@ -62,9 +64,9 @@ def can_handle(_data) -> bool: base = _data.get("base_file") if not base: return False - return ( - base.endswith(".sld") if isinstance(base, str) else base.name.endswith(".sld") - ) and MetadataFileHandler.can_handle(_data) + return (base.endswith(".sld") if isinstance(base, str) else base.name.endswith(".sld")) and _data.get( + "action", None + ) == ira.RESOURCE_STYLE_UPLOAD.value @staticmethod def is_valid(files, user, **kwargs): diff --git a/geonode/upload/handlers/sld/tests.py b/geonode/upload/handlers/sld/tests.py index 1864ba2dee1..2cbc7931382 100644 --- a/geonode/upload/handlers/sld/tests.py +++ b/geonode/upload/handlers/sld/tests.py @@ -45,7 +45,7 @@ def setUpClass(cls): cls.valid_files = { "base_file": "/tmp/test_sld.sld", "sld_file": "/tmp/test_sld.sld", - "source": "resource_file_upload", + "action": "resource_style_upload", } cls.owner = get_user_model().objects.first() cls.layer = create_single_dataset(name="sld_dataset", owner=cls.owner) @@ -59,8 +59,8 @@ def test_task_list_is_the_expected_one(self): "start_import", "geonode.upload.import_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 2) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["resource_style_upload"]), 2) + self.assertTupleEqual(expected, self.handler.TASKS["resource_style_upload"]) def test_is_valid_should_raise_exception_if_the_sld_is_invalid(self): with self.assertRaises(InvalidSldException) as _exc: diff --git a/geonode/upload/handlers/tiles3d/handler.py b/geonode/upload/handlers/tiles3d/handler.py index 43cbe1ba498..08a25443652 100755 --- a/geonode/upload/handlers/tiles3d/handler.py +++ b/geonode/upload/handlers/tiles3d/handler.py @@ -42,8 +42,8 @@ class Tiles3DFileHandler(BaseVectorFileHandler): It must provide the task_lists required to comple the upload """ - ACTIONS = { - exa.IMPORT.value: ( + TASKS = { + exa.UPLOAD.value: ( "start_import", "geonode.upload.import_resource", "geonode.upload.create_geonode_resource", @@ -62,10 +62,14 @@ class Tiles3DFileHandler(BaseVectorFileHandler): def supported_file_extension_config(self): return { "id": "3dtiles", - "label": "3D Tiles", - "format": "vector", - "ext": ["json"], - "optional": ["xml", "sld"], + "formats": [ + { + "label": "3D Tiles", + "required_ext": ["zip"], + } + ], + "actions": list(self.TASKS.keys()), + "type": "vector", } @staticmethod @@ -151,7 +155,7 @@ def extract_params_from_data(_data, action=None): return { "skip_existing_layers": _data.pop("skip_existing_layers", "False"), "store_spatial_file": _data.pop("store_spatial_files", "True"), - "source": _data.pop("source", "upload"), + "action": _data.pop("action", "upload"), "original_zip_name": _data.pop("original_zip_name", None), "overwrite_existing_layer": _data.pop("overwrite_existing_layer", False), }, _data @@ -198,7 +202,7 @@ def import_resource(self, files: dict, execution_id: str, **kwargs) -> str: "geonode.upload.import_resource", layer_name, alternate, - exa.IMPORT.value, + exa.UPLOAD.value, ) ) return layer_name, alternate, execution_id diff --git a/geonode/upload/handlers/tiles3d/tests.py b/geonode/upload/handlers/tiles3d/tests.py index 8bbfc66cb2e..6aa3982d3e2 100755 --- a/geonode/upload/handlers/tiles3d/tests.py +++ b/geonode/upload/handlers/tiles3d/tests.py @@ -58,16 +58,16 @@ def test_task_list_is_the_expected_one(self): "geonode.upload.import_resource", "geonode.upload.create_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 3) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["upload"]), 3) + self.assertTupleEqual(expected, self.handler.TASKS["upload"]) def test_task_list_is_the_expected_one_copy(self): expected = ( "start_copy", "geonode.upload.copy_geonode_resource", ) - self.assertEqual(len(self.handler.ACTIONS["copy"]), 2) - self.assertTupleEqual(expected, self.handler.ACTIONS["copy"]) + self.assertEqual(len(self.handler.TASKS["copy"]), 2) + self.assertTupleEqual(expected, self.handler.TASKS["copy"]) def test_is_valid_should_raise_exception_if_the_parallelism_is_met(self): parallelism, created = UploadParallelismLimit.objects.get_or_create(slug="default_max_parallel_uploads") @@ -190,10 +190,14 @@ def test_supported_file_extension_config(self): """ expected = { "id": "3dtiles", - "label": "3D Tiles", - "format": "vector", - "ext": ["json"], - "optional": ["xml", "sld"], + "formats": [ + { + "label": "3D Tiles", + "required_ext": ["zip"], + } + ], + "actions": list(Tiles3DFileHandler.TASKS.keys()), + "type": "vector", } actual = self.handler.supported_file_extension_config self.assertDictEqual(actual, expected) diff --git a/geonode/upload/handlers/utils.py b/geonode/upload/handlers/utils.py index e6ea039d3f2..f4908d43b5a 100644 --- a/geonode/upload/handlers/utils.py +++ b/geonode/upload/handlers/utils.py @@ -16,7 +16,6 @@ # along with this program. If not, see . # ######################################################################### -import enum import hashlib from django.contrib.auth import get_user_model @@ -32,12 +31,6 @@ logger = logging.getLogger("importer") -# TODO this part should be improved when we will drop the legacy upload templates -class UploadSourcesEnum(enum.Enum): - upload = "upload" # used in the default upload flow - resource_file_upload = "resource_file_upload" # source used for the single resource metadata upload - - STANDARD_TYPE_MAPPING = { "Integer64": "django.db.models.IntegerField", "Integer": "django.db.models.IntegerField", diff --git a/geonode/upload/handlers/xml/handler.py b/geonode/upload/handlers/xml/handler.py index a6f6340689f..58c64ac3a8a 100644 --- a/geonode/upload/handlers/xml/handler.py +++ b/geonode/upload/handlers/xml/handler.py @@ -22,6 +22,7 @@ from geonode.upload.handlers.common.metadata import MetadataFileHandler from geonode.upload.handlers.xml.exceptions import InvalidXmlException from owslib.etree import etree as dlxml +from geonode.upload.utils import ImporterRequestAction as ira logger = logging.getLogger("importer") @@ -32,25 +33,26 @@ class XMLFileHandler(MetadataFileHandler): It must provide the task_lists required to comple the upload """ + TASKS = { + ira.RESOURCE_METADATA_UPLOAD.value: ("start_import", "geonode.upload.import_resource"), + ira.ROLLBACK.value: ( + "start_rollback", + "geonode.upload.rollback", + ), + } + @property def supported_file_extension_config(self): return { "id": "xml", - "label": "XML Metadata File", - "format": "metadata", - "ext": ["xml"], - "mimeType": ["application/json"], - "needsFiles": [ - "shp", - "prj", - "dbf", - "shx", - "csv", - "tiff", - "zip", - "sld", - "geojson", + "formats": [ + { + "label": "XML Metadata File (XML - ISO, FGDC, ebRIM, Dublin Core)", + "required_ext": ["xml"], + } ], + "actions": list(self.TASKS.keys()), + "type": "metadata", } @staticmethod @@ -62,9 +64,9 @@ def can_handle(_data) -> bool: base = _data.get("base_file") if not base: return False - return ( - base.endswith(".xml") if isinstance(base, str) else base.name.endswith(".xml") - ) and MetadataFileHandler.can_handle(_data) + return (base.endswith(".xml") if isinstance(base, str) else base.name.endswith(".xml")) and _data.get( + "action", None + ) == ira.RESOURCE_METADATA_UPLOAD.value @staticmethod def is_valid(files, user=None, **kwargs): diff --git a/geonode/upload/handlers/xml/serializer.py b/geonode/upload/handlers/xml/serializer.py index a28ddd2118a..19bb53fb61c 100644 --- a/geonode/upload/handlers/xml/serializer.py +++ b/geonode/upload/handlers/xml/serializer.py @@ -27,9 +27,9 @@ class Meta: ref_name = "MetadataFileSerializer" model = ResourceBase view_name = "importer_upload" - fields = ("overwrite_existing_layer", "resource_pk", "base_file", "source") + fields = ("overwrite_existing_layer", "resource_pk", "base_file", "action") base_file = serializers.FileField() overwrite_existing_layer = serializers.BooleanField(required=False, default=True) resource_pk = serializers.CharField(required=True) - source = serializers.CharField(required=False, default="resource_file_upload") + action = serializers.CharField(required=True) diff --git a/geonode/upload/handlers/xml/tests.py b/geonode/upload/handlers/xml/tests.py index af9916a1fc8..b96147f4d94 100644 --- a/geonode/upload/handlers/xml/tests.py +++ b/geonode/upload/handlers/xml/tests.py @@ -44,7 +44,7 @@ def setUpClass(cls): cls.valid_files = { "base_file": "/tmp/test_xml.xml", "xml_file": "/tmp/test_xml.xml", - "source": "resource_file_upload", + "action": "resource_metadata_upload", } cls.owner = get_user_model().objects.first() cls.layer = create_single_dataset(name="extruded_polygon", owner=cls.owner) @@ -58,8 +58,8 @@ def test_task_list_is_the_expected_one(self): "start_import", "geonode.upload.import_resource", ) - self.assertEqual(len(self.handler.ACTIONS["import"]), 2) - self.assertTupleEqual(expected, self.handler.ACTIONS["import"]) + self.assertEqual(len(self.handler.TASKS["resource_metadata_upload"]), 2) + self.assertTupleEqual(expected, self.handler.TASKS["resource_metadata_upload"]) def test_is_valid_should_raise_exception_if_the_xml_is_invalid(self): with self.assertRaises(InvalidXmlException) as _exc: diff --git a/geonode/upload/orchestrator.py b/geonode/upload/orchestrator.py index 1611e4f6d91..a3d9ecab88a 100644 --- a/geonode/upload/orchestrator.py +++ b/geonode/upload/orchestrator.py @@ -48,19 +48,29 @@ class ImportOrchestrator: """ + def get_handler_registry(self): + return BaseHandler.get_registry() + def get_handler(self, _data) -> Optional[BaseHandler]: """ If is part of the supported format, return the handler which can handle the import otherwise return None """ - for handler in BaseHandler.get_registry(): - if handler.can_handle(_data): - return handler() - logger.error("Handler not found") + for handler in self.get_handler_registry(): + can_handle = handler.can_handle(_data) + match can_handle: + case True: + return handler() + case False: + logger.info( + f"The handler {str(handler)} cannot manage the requested action: {_data.get('action', None)}" + ) + + logger.error("No handlers found for this dataset type/action") return None def get_serializer(self, _data) -> serializers.Serializer: - for handler in BaseHandler.get_registry(): + for handler in self.get_handler_registry(): _serializer = handler.has_serializer(_data) if _serializer: return _serializer @@ -77,7 +87,7 @@ def load_handler(self, module_path): raise ImportException(detail=f"The handler is not available: {module_path}") def load_handler_by_id(self, handler_id): - for handler in BaseHandler.get_registry(): + for handler in self.get_handler_registry(): if handler().id == handler_id: return handler logger.error("Handler not found") @@ -300,7 +310,6 @@ def create_execution_request( input_params=input_params, action=action, name=name, - source=source, ) return execution.exec_id diff --git a/geonode/upload/tests/end2end/test_end2end.py b/geonode/upload/tests/end2end/test_end2end.py index 3d93c65f075..f0a493b144e 100644 --- a/geonode/upload/tests/end2end/test_end2end.py +++ b/geonode/upload/tests/end2end/test_end2end.py @@ -176,9 +176,7 @@ class ImporterGeoPackageImportTest(BaseImporterEndToEndTest): def test_import_geopackage(self): self._cleanup_layers(name="stazioni_metropolitana") - payload = { - "base_file": open(self.valid_gkpg, "rb"), - } + payload = {"base_file": open(self.valid_gkpg, "rb"), "action": "upload"} initial_name = "stazioni_metropolitana" self._assertimport(payload, initial_name) self._cleanup_layers(name="stazioni_metropolitana") @@ -188,14 +186,10 @@ def test_import_geopackage(self): def test_import_gpkg_overwrite(self): self._cleanup_layers(name="stazioni_metropolitana") initial_name = "stazioni_metropolitana" - payload = { - "base_file": open(self.valid_gkpg, "rb"), - } + payload = {"base_file": open(self.valid_gkpg, "rb"), "action": "upload"} prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) - payload = { - "base_file": open(self.valid_gkpg, "rb"), - } + payload = {"base_file": open(self.valid_gkpg, "rb"), "action": "upload"} payload["overwrite_existing_layer"] = True payload["resource_pk"] = prev_dataset.pk self._assertimport(payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated) @@ -210,9 +204,7 @@ class ImporterNoCRSImportTest(BaseImporterEndToEndTest): def test_import_geopackage_with_no_crs_table(self): self._cleanup_layers(name="mattia_test") - payload = { - "base_file": open(self.no_crs_gpkg, "rb"), - } + payload = {"base_file": open(self.no_crs_gpkg, "rb"), "action": "upload"} initial_name = "mattia_test" with self.assertLogs(level="ERROR") as _log: self._assertimport(payload, initial_name) @@ -235,10 +227,7 @@ def test_import_geopackage_with_no_crs_table_should_raise_error_if_all_layer_are _select_valid_layers.return_value = [] self._cleanup_layers(name="mattia_test") - payload = { - "base_file": open(self.no_crs_gpkg, "rb"), - "store_spatial_file": True, - } + payload = {"base_file": open(self.no_crs_gpkg, "rb"), "store_spatial_file": True, "action": "upload"} with self.assertLogs(level="ERROR") as _log: self.client.force_login(self.admin) @@ -261,9 +250,7 @@ def test_import_geojson(self): self._cleanup_layers(name="valid") - payload = { - "base_file": open(self.valid_geojson, "rb"), - } + payload = {"base_file": open(self.valid_geojson, "rb"), "action": "upload"} initial_name = "valid" self._assertimport(payload, initial_name) @@ -273,14 +260,10 @@ def test_import_geojson(self): @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") def test_import_geojson_overwrite(self): self._cleanup_layers(name="valid") - payload = { - "base_file": open(self.valid_geojson, "rb"), - } + payload = {"base_file": open(self.valid_geojson, "rb"), "action": "upload"} initial_name = "valid" prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) - payload = { - "base_file": open(self.valid_geojson, "rb"), - } + payload = {"base_file": open(self.valid_geojson, "rb"), "action": "upload"} payload["overwrite_existing_layer"] = True payload["resource_pk"] = prev_dataset.pk self._assertimport(payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated) @@ -294,9 +277,7 @@ class ImporterGCSVImportTest(BaseImporterEndToEndTest): def test_import_geojson(self): self._cleanup_layers(name="valid") - payload = { - "base_file": open(self.valid_csv, "rb"), - } + payload = {"base_file": open(self.valid_csv, "rb"), "action": "upload"} initial_name = "valid" self._assertimport(payload, initial_name) self._cleanup_layers(name="valid") @@ -305,15 +286,11 @@ def test_import_geojson(self): @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") def test_import_csv_overwrite(self): self._cleanup_layers(name="valid") - payload = { - "base_file": open(self.valid_csv, "rb"), - } + payload = {"base_file": open(self.valid_csv, "rb"), "action": "upload"} initial_name = "valid" prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) - payload = { - "base_file": open(self.valid_csv, "rb"), - } + payload = {"base_file": open(self.valid_csv, "rb"), "action": "upload"} initial_name = "valid" payload["overwrite_existing_layer"] = True payload["resource_pk"] = prev_dataset.pk @@ -326,9 +303,7 @@ class ImporterKMLImportTest(BaseImporterEndToEndTest): @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") def test_import_kml(self): self._cleanup_layers(name="sample_point_dataset") - payload = { - "base_file": open(self.valid_kml, "rb"), - } + payload = {"base_file": open(self.valid_kml, "rb"), "action": "upload"} initial_name = "sample_point_dataset" self._assertimport(payload, initial_name) self._cleanup_layers(name="sample_point_dataset") @@ -339,14 +314,10 @@ def test_import_kml_overwrite(self): initial_name = "sample_point_dataset" self._cleanup_layers(name="sample_point_dataset") - payload = { - "base_file": open(self.valid_kml, "rb"), - } + payload = {"base_file": open(self.valid_kml, "rb"), "action": "upload"} prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) - payload = { - "base_file": open(self.valid_kml, "rb"), - } + payload = {"base_file": open(self.valid_kml, "rb"), "action": "upload"} payload["overwrite_existing_layer"] = True payload["resource_pk"] = prev_dataset.pk self._assertimport(payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated) @@ -359,6 +330,7 @@ class ImporterShapefileImportTest(BaseImporterEndToEndTest): def test_import_shapefile(self): self._cleanup_layers(name="air_Runways") payload = {_filename: open(_file, "rb") for _filename, _file in self.valid_shp.items()} + payload["action"] = "upload" initial_name = "air_Runways" self._assertimport(payload, initial_name) self._cleanup_layers(name="air_Runways") @@ -369,11 +341,13 @@ def test_import_shapefile_overwrite(self): self._cleanup_layers(name="air_Runways") payload = {_filename: open(_file, "rb") for _filename, _file in self.valid_shp.items()} + payload["action"] = "upload" initial_name = "air_Runways" prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) payload = {_filename: open(_file, "rb") for _filename, _file in self.valid_shp.items()} payload["overwrite_existing_layer"] = True payload["resource_pk"] = prev_dataset.pk + payload["action"] = "upload" self._assertimport( payload, initial_name, overwrite=True, last_update=prev_dataset.last_updated, keep_resource=True ) @@ -386,9 +360,7 @@ class ImporterRasterImportTest(BaseImporterEndToEndTest): def test_import_raster(self): self._cleanup_layers(name="test_raster") - payload = { - "base_file": open(self.valid_tif, "rb"), - } + payload = {"base_file": open(self.valid_tif, "rb"), "action": "upload"} initial_name = "test_raster" self._assertimport(payload, initial_name) self._cleanup_layers(name="test_raster") @@ -399,14 +371,10 @@ def test_import_raster_overwrite(self): initial_name = "test_raster" self._cleanup_layers(name="test_raster") - payload = { - "base_file": open(self.valid_tif, "rb"), - } + payload = {"base_file": open(self.valid_tif, "rb"), "action": "upload"} prev_dataset = self._assertimport(payload, initial_name, keep_resource=True) - payload = { - "base_file": open(self.valid_tif, "rb"), - } + payload = {"base_file": open(self.valid_tif, "rb"), "action": "upload"} initial_name = "test_raster" payload["overwrite_existing_layer"] = True payload["resource_pk"] = prev_dataset.pk @@ -422,6 +390,7 @@ def test_import_3dtiles(self): "url": "https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples/main/1.1/TilesetWithFullMetadata/tileset.json", "title": "Remote Title", "type": "3dtiles", + "action": "upload", } initial_name = "remote_title" assert_payload = { @@ -438,6 +407,7 @@ def test_import_3dtiles_overwrite(self): "url": "https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples/main/1.1/TilesetWithFullMetadata/tileset.json", "title": "Remote Title", "type": "3dtiles", + "action": "upload", } initial_name = "remote_title" assert_payload = { @@ -485,6 +455,7 @@ def test_import_wms(self): "type": "wms", "lookup": resource_to_take, "parse_remote_metadata": True, + "action": "upload", } initial_name = res.title assert_payload = { diff --git a/geonode/upload/tests/end2end/test_end2end_copy.py b/geonode/upload/tests/end2end/test_end2end_copy.py index 630e16962b4..a66eacc09ca 100644 --- a/geonode/upload/tests/end2end/test_end2end_copy.py +++ b/geonode/upload/tests/end2end/test_end2end_copy.py @@ -85,6 +85,7 @@ def _assertCloning(self, initial_name): # defining the payload payload = QueryDict("", mutable=True) payload.update({"defaults": '{"title":"title_of_the_cloned_resource"}'}) + payload["action"] = "copy" # calling the endpoint response = self.client.put(_url, data=payload, content_type="application/json") @@ -113,6 +114,7 @@ def _assertCloning(self, initial_name): self.assertTrue(schema_entity.name in [y.name for y in resources]) def _import_resource(self, payload, initial_name): + payload["action"] = "upload" _url = reverse("importer_upload") self.client.force_login(get_user_model().objects.get(username="admin")) @@ -147,9 +149,7 @@ class ImporterCopyEnd2EndGpkgTest(BaseClassEnd2End): ) @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") def test_copy_dataset_from_geopackage(self): - payload = { - "base_file": open(self.valid_gkpg, "rb"), - } + payload = {"base_file": open(self.valid_gkpg, "rb"), "action": "copy"} initial_name = "stazioni_metropolitana" # first we need to import a resource with transaction.atomic(): @@ -168,9 +168,7 @@ class ImporterCopyEnd2EndGeoJsonTest(BaseClassEnd2End): ) @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") def test_copy_dataset_from_geojson(self): - payload = { - "base_file": open(self.valid_geojson, "rb"), - } + payload = {"base_file": open(self.valid_geojson, "rb"), "action": "copy"} initial_name = "valid" # first we need to import a resource with transaction.atomic(): @@ -189,6 +187,7 @@ class ImporterCopyEnd2EndShapeFileTest(BaseClassEnd2End): @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") def test_copy_dataset_from_shapefile(self): payload = {_filename: open(_file, "rb") for _filename, _file in self.valid_shp.items()} + payload["action"] = "copy" initial_name = "air_runways" # first we need to import a resource with transaction.atomic(): @@ -206,9 +205,7 @@ class ImporterCopyEnd2EndKMLTest(BaseClassEnd2End): ) @override_settings(GEODATABASE_URL=f"{geourl.split('/geonode_data')[0]}/test_geonode_data") def test_copy_dataset_from_kml(self): - payload = { - "base_file": open(self.valid_kml, "rb"), - } + payload = {"base_file": open(self.valid_kml, "rb"), "action": "copy"} initial_name = "sample_point_dataset" # first we need to import a resource with transaction.atomic(): diff --git a/geonode/upload/tests/unit/test_dastore.py b/geonode/upload/tests/unit/test_dastore.py index 361b67f028a..fd9b504c3cf 100644 --- a/geonode/upload/tests/unit/test_dastore.py +++ b/geonode/upload/tests/unit/test_dastore.py @@ -37,11 +37,10 @@ def setUp(self): user=self.user, func_name="create", step="create", - action="import", + action="upload", input_params={ **{"handler_module_path": "geonode.upload.handlers.gpkg.handler.GPKGFileHandler"}, }, - source="importer_copy", ) self.datastore = DataStoreManager( self.files, "geonode.upload.handlers.gpkg.handler.GPKGFileHandler", self.user, execution_id @@ -51,9 +50,8 @@ def setUp(self): user=self.user, func_name="create", step="create", - action="import", + action="upload", input_params={"url": "https://geosolutionsgroup.com"}, - source="importer_copy", ) self.datastore_url = DataStoreManager( self.files, "geonode.upload.handlers.common.remote.BaseRemoteResourceHandler", self.user, execution_id_url diff --git a/geonode/upload/tests/unit/test_orchestrator.py b/geonode/upload/tests/unit/test_orchestrator.py index b4d3871c042..a1f11c1f6ae 100644 --- a/geonode/upload/tests/unit/test_orchestrator.py +++ b/geonode/upload/tests/unit/test_orchestrator.py @@ -44,7 +44,7 @@ def setUpClass(cls): cls.orchestrator = ImportOrchestrator() def test_get_handler(self): - _data = {"base_file": "file.gpkg", "source": "upload"} + _data = {"base_file": "file.gpkg", "action": "upload"} actual = self.orchestrator.get_handler(_data) self.assertIsNotNone(actual) self.assertEqual("geonode.upload.handlers.gpkg.handler.GPKGFileHandler", str(actual)) @@ -102,12 +102,13 @@ def test_create_execution_request(self): } exec_id = self.orchestrator.create_execution_request( user=get_user_model().objects.first(), - func_name=next(iter(handler.get_task_list(action="import"))), - step=next(iter(handler.get_task_list(action="import"))), + func_name=next(iter(handler.get_task_list(action="upload"))), + step=next(iter(handler.get_task_list(action="upload"))), input_params={ "files": {"base_file": "/tmp/file.txt"}, "store_spatial_files": True, }, + action="upload", ) exec_obj = ExecutionRequest.objects.filter(exec_id=exec_id).first() self.assertEqual(count + 1, ExecutionRequest.objects.count()) @@ -120,7 +121,7 @@ def test_perform_next_step(self, mock_celery): handler = self.orchestrator.load_handler("geonode.upload.handlers.gpkg.handler.GPKGFileHandler") _id = self.orchestrator.create_execution_request( user=get_user_model().objects.first(), - func_name=next(iter(handler.get_task_list(action="import"))), + func_name=next(iter(handler.get_task_list(action="upload"))), step="start_import", # adding the first step for the GPKG file input_params={ "files": {"base_file": "/tmp/file.txt"}, @@ -130,7 +131,7 @@ def test_perform_next_step(self, mock_celery): # test under tests self.orchestrator.perform_next_step( _id, - "import", + "upload", step="start_import", handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", ) @@ -144,17 +145,18 @@ def test_perform_last_import_step(self, mock_celery): handler = self.orchestrator.load_handler("geonode.upload.handlers.gpkg.handler.GPKGFileHandler") _id = self.orchestrator.create_execution_request( user=get_user_model().objects.first(), - func_name=next(iter(handler.get_task_list(action="import"))), + func_name=next(iter(handler.get_task_list(action="upload"))), step="geonode.upload.create_geonode_resource", # adding the first step for the GPKG file input_params={ "files": {"base_file": "/tmp/file.txt"}, "store_spatial_files": True, }, + action="upload", ) # test under tests self.orchestrator.perform_next_step( _id, - "import", + "upload", step="geonode.upload.create_geonode_resource", handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", ) @@ -167,7 +169,7 @@ def test_perform_with_error_set_invalid_status(self, mock_celery): handler = self.orchestrator.load_handler("geonode.upload.handlers.gpkg.handler.GPKGFileHandler") _id = self.orchestrator.create_execution_request( user=get_user_model().objects.first(), - func_name=next(iter(handler.get_task_list(action="import"))), + func_name=next(iter(handler.get_task_list(action="upload"))), step="start_import", # adding the first step for the GPKG file input_params={ "files": {"base_file": "/tmp/file.txt"}, @@ -178,7 +180,7 @@ def test_perform_with_error_set_invalid_status(self, mock_celery): with self.assertRaises(Exception): self.orchestrator.perform_next_step( _id, - "import", + "upload", step="start_import", handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", ) diff --git a/geonode/upload/tests/unit/test_publisher.py b/geonode/upload/tests/unit/test_publisher.py index 0117d0a5760..9d7f7e06e10 100644 --- a/geonode/upload/tests/unit/test_publisher.py +++ b/geonode/upload/tests/unit/test_publisher.py @@ -60,7 +60,7 @@ def test_extract_resource_name_and_crs(self): """ values_found = self.publisher.extract_resource_to_publish( files={"base_file": self.gpkg_path}, - action="import", + action="upload", layer_name="stazioni_metropolitana", ) expected = {"crs": "EPSG:32632", "name": "stazioni_metropolitana"} @@ -75,7 +75,7 @@ def test_extract_resource_name_and_crs_return_empty_if_the_file_does_not_exists( """ values_found = self.publisher.extract_resource_to_publish( files={"base_file": "/wrong/path/file.gpkg"}, - action="import", + action="upload", layer_name="stazioni_metropolitana", ) self.assertListEqual([], values_found) diff --git a/geonode/upload/tests/unit/test_task.py b/geonode/upload/tests/unit/test_task.py index 1f374da4112..6e67b186091 100644 --- a/geonode/upload/tests/unit/test_task.py +++ b/geonode/upload/tests/unit/test_task.py @@ -120,7 +120,7 @@ def test_import_resource_should_rase_exp_if_is_invalid( with self.assertRaises(InvalidInputFileException) as _exc: import_resource( str(exec_id), - action=ExecutionRequestAction.IMPORT.value, + action=ExecutionRequestAction.UPLOAD.value, handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", ) expected_msg = f"Invalid format type. Request: {str(exec_id)}" @@ -151,7 +151,7 @@ def test_import_resource_should_work( import_resource( str(exec_id), resource_type="gpkg", - action=ExecutionRequestAction.IMPORT.value, + action=ExecutionRequestAction.UPLOAD.value, handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", ) @@ -178,7 +178,7 @@ def test_publish_resource_should_work( step_name="publish_resource", layer_name="dataset3", alternate="alternate_dataset3", - action=ExecutionRequestAction.IMPORT.value, + action=ExecutionRequestAction.UPLOAD.value, handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", ) @@ -224,7 +224,7 @@ def test_publish_resource_if_overwrite_should_call_the_publishing( step_name="publish_resource", layer_name="dataset3", alternate="alternate_dataset3", - action=ExecutionRequestAction.IMPORT.value, + action=ExecutionRequestAction.UPLOAD.value, handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", ) @@ -276,7 +276,7 @@ def test_publish_resource_if_overwrite_should_not_call_the_publishing( step_name="publish_resource", layer_name="dataset3", alternate="alternate_dataset3", - action=ExecutionRequestAction.IMPORT.value, + action=ExecutionRequestAction.UPLOAD.value, handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", ) @@ -304,7 +304,7 @@ def test_create_geonode_resource(self, import_orchestrator): layer_name="foo_dataset", alternate="alternate_foo_dataset", handler_module_path="geonode.upload.handlers.gpkg.handler.GPKGFileHandler", - action="import", + action="upload", ) # Evaluation @@ -401,7 +401,7 @@ def test_rollback_works_as_expected_vector_step( user=get_user_model().objects.get(username=self.user), func_name="dummy_func", step=conf[0], # step name - action="import", + action="upload", input_params={ "files": {"base_file": self.existing_file}, "store_spatial_files": True, @@ -456,7 +456,7 @@ def test_rollback_works_as_expected_raster( user=get_user_model().objects.get(username=self.user), func_name="dummy_func", step=conf[0], # step name - action="import", + action="upload", input_params={ "files": {"base_file": "/tmp/filepath"}, "store_spatial_files": True, diff --git a/geonode/upload/utils.py b/geonode/upload/utils.py index a82147b6253..0e3750e2ece 100644 --- a/geonode/upload/utils.py +++ b/geonode/upload/utils.py @@ -50,6 +50,9 @@ def get_max_upload_parallelism_limit(slug): class ImporterRequestAction(enum.Enum): ROLLBACK = _("rollback") + RESOURCE_METADATA_UPLOAD = _("resource_metadata_upload") + RESOURCE_STYLE_UPLOAD = _("resource_style_upload") + REPLACE = _("replace") def error_handler(exc, exec_id=None): diff --git a/geonode/utils.py b/geonode/utils.py index 514061ca7f0..60261075fa6 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -29,7 +29,6 @@ import requests import tempfile import ipaddress -import itertools import traceback from lxml import etree @@ -1706,13 +1705,35 @@ def get_geonode_app_types(): def get_supported_datasets_file_types(): from django.conf import settings as gn_settings + from geonode.upload.orchestrator import orchestrator """ Return a list of all supported file type in geonode If one of the type provided in the custom type exists in the default is going to override it """ - default_types = settings.SUPPORTED_DATASET_FILE_TYPES + _available_settings = [ + module().supported_file_extension_config + for module in orchestrator.get_handler_registry() + if module().supported_file_extension_config + ] + # injecting the new config required for FE + default_types = [ + { + "id": "zip", + "formats": [ + { + "label": "Zip Archive", + "required_ext": ["zip"], + "optional_ext": ["xml", "sld"], + } + ], + "actions": ["upload", "replace"], + "type": "archive", + } + ] + default_types.extend(_available_settings) + types_module = ( gn_settings.ADDITIONAL_DATASET_FILE_TYPES if hasattr(gn_settings, "ADDITIONAL_DATASET_FILE_TYPES") else [] ) @@ -1730,7 +1751,7 @@ def get_supported_datasets_file_types(): (weight[1], resource_type) for resource_type in supported_types for weight in formats_order - if resource_type.get("format") in weight[0] + if resource_type.get("type") in weight[0] ) # Flatten the list @@ -1738,10 +1759,18 @@ def get_supported_datasets_file_types(): other_resource_types = [ resource_type for resource_type in supported_types - if resource_type.get("format") is None or resource_type.get("format") not in [f[0] for f in formats_order] + if resource_type.get("type") is None or resource_type.get("type") not in [f[0] for f in formats_order] ] return ordered_resource_types + other_resource_types def get_allowed_extensions(): - return list(itertools.chain.from_iterable([_type["ext"] for _type in get_supported_datasets_file_types()])) + """ + The main extension is rappresented by the position 0 of the configuration + that the handlers returns + """ + allowed_extention = [] + for _type in get_supported_datasets_file_types(): + for val in _type["formats"]: + allowed_extention.append(val["required_ext"][0]) + return list(set(allowed_extention)) From 5d5a99a6183c9d5fc626072ce68c32fe3f292ffe Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:48:42 +0100 Subject: [PATCH 22/61] [Fixes #12679] Add translations for all resources (#12682) --- geonode/locale/de/LC_MESSAGES/django.mo | Bin 156837 -> 156881 bytes geonode/locale/de/LC_MESSAGES/django.po | 3 +++ geonode/locale/it/LC_MESSAGES/django.mo | Bin 161424 -> 161471 bytes geonode/locale/it/LC_MESSAGES/django.po | 3 +++ 4 files changed, 6 insertions(+) diff --git a/geonode/locale/de/LC_MESSAGES/django.mo b/geonode/locale/de/LC_MESSAGES/django.mo index 6f83b9fc2f3647e48b3f7da1d9d518596386b16a..64672f7c4586b3ef1c53db66915e69d3c57c4ec0 100644 GIT binary patch delta 32802 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 diff --git a/geonode/locale/de/LC_MESSAGES/django.po b/geonode/locale/de/LC_MESSAGES/django.po index 01910e54f14..7db224b06e3 100644 --- a/geonode/locale/de/LC_MESSAGES/django.po +++ b/geonode/locale/de/LC_MESSAGES/django.po @@ -6853,3 +6853,6 @@ msgstr "Zugriff verweigert" #~ msgid "Thesaurus" #~ msgstr "Thesaurus" + +msgid "All resources" +msgstr "Alle ressourcen" \ No newline at end of file diff --git a/geonode/locale/it/LC_MESSAGES/django.mo b/geonode/locale/it/LC_MESSAGES/django.mo index cc42b56d611c29c8119d59fe9df28ca870d02a55..1df6d3c5a4d4f9a581d2e68703425fabeab46446 100644 GIT binary patch delta 34056 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 delta 34040 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 n@ Date: Tue, 12 Nov 2024 12:45:33 +0100 Subject: [PATCH 23/61] [Fixes #12616] Document upload permissions fix (#12707) --- .clabot | 3 ++- geonode/documents/api/views.py | 2 +- geonode/documents/views.py | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.clabot b/.clabot index 3c433292143..2e706ddd684 100644 --- a/.clabot +++ b/.clabot @@ -77,6 +77,7 @@ "ahmdthr", "fvicent", "RegisSinjari", - "Gpetrak" + "Gpetrak", + "kilichenko-pixida" ] } diff --git a/geonode/documents/api/views.py b/geonode/documents/api/views.py index d68ca3a09b8..171be79be7b 100644 --- a/geonode/documents/api/views.py +++ b/geonode/documents/api/views.py @@ -137,7 +137,7 @@ def perform_create(self, serializer): resource.set_missing_info() resourcebase_post_save(resource.get_real_instance()) - resource_manager.set_permissions(None, instance=resource, permissions=None, created=True) + resource.set_default_permissions(owner=self.request.user, created=True) resource.handle_moderated_uploads() resource_manager.set_thumbnail(resource.uuid, instance=resource, overwrite=False) return resource diff --git a/geonode/documents/views.py b/geonode/documents/views.py index a73ae674f42..3105d9c084d 100644 --- a/geonode/documents/views.py +++ b/geonode/documents/views.py @@ -200,9 +200,7 @@ def form_valid(self, form): ) self.object.handle_moderated_uploads() - resource_manager.set_permissions( - None, instance=self.object, permissions=form.cleaned_data["permissions"], created=True - ) + self.object.set_default_permissions(owner=self.request.user, created=True) abstract = None date = None From 26907531a4f33f4d81a3dd4129df0d6ab4c9d92c Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:55:26 +0100 Subject: [PATCH 24/61] [Fixes #12728] 3dtiles download does not keep the folder structure (#12729) --- geonode/assets/local.py | 6 ++---- geonode/proxy/views.py | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/geonode/assets/local.py b/geonode/assets/local.py index aa8e486c485..97e4a7c7d25 100644 --- a/geonode/assets/local.py +++ b/geonode/assets/local.py @@ -7,7 +7,7 @@ from django.http import HttpResponse, StreamingHttpResponse from django.urls import reverse from django_downloadview import DownloadResponse -from zipstream import ZipStream, walk +from zipstream import ZipStream from geonode.assets.handlers import asset_handler_registry, AssetHandlerInterface, AssetDownloadHandlerInterface from geonode.assets.models import LocalAsset @@ -261,9 +261,7 @@ def create_response( match attachment: case True: logger.info(f"Zipping file '{localfile}' with name '{orig_base}'") - zs = ZipStream(sized=True) - for filepath in walk(LocalAssetHandler._get_managed_dir(asset)): - zs.add_path(filepath, os.path.basename(filepath)) + zs = ZipStream(sized=True).from_path(LocalAssetHandler._get_managed_dir(asset), arcname="/") # closing zip for all contents to be written return StreamingHttpResponse( zs, diff --git a/geonode/proxy/views.py b/geonode/proxy/views.py index 9f883ad9bbe..fe7587c5d18 100644 --- a/geonode/proxy/views.py +++ b/geonode/proxy/views.py @@ -275,7 +275,7 @@ def download(request, resourceid, sender=Dataset): register_event(request, "download", instance) folder = os.path.dirname(dataset_files[0]) - zs = ZipStream.from_path(folder) + zs = ZipStream.from_path(folder, arcname="/") return StreamingHttpResponse( zs, content_type="application/zip", diff --git a/requirements.txt b/requirements.txt index 0d8b8a9399c..a0c0cec4f33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ rdflib==6.3.2 smart_open==7.0.4 PyMuPDF==1.24.3 defusedxml==0.7.1 -zipstream-ng==1.7.1 +zipstream-ng==1.8.0 # Django Apps django-allauth==0.63.6 diff --git a/setup.cfg b/setup.cfg index c5c3a71400e..eb72dbeb8bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,7 +47,7 @@ install_requires = Deprecated==1.2.14 wrapt==1.16.0 jsonschema==4.22.0 - zipstream-ng==1.7.1 + zipstream-ng==1.8.0 schema==0.7.7 rdflib==6.3.2 smart_open==7.0.4 From a347483ec76d6daa63a806938582e675c437fdfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 00:24:48 +0000 Subject: [PATCH 25/61] build(deps): bump django from 4.2.9 to 4.2.16 Bumps [django](https://github.com/django/django) from 4.2.9 to 4.2.16. - [Commits](https://github.com/django/django/compare/4.2.9...4.2.16) --- updated-dependencies: - dependency-name: django dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a0c0cec4f33..c6b4132b859 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Pillow==10.4.0 lxml==5.2.1 psycopg2==2.9.9 -Django==4.2.9 +Django==4.2.16 # Other amqp==5.2.0 From e46f092ed9271d1f2c56a55dad5464b087d618df Mon Sep 17 00:00:00 2001 From: Mattia Date: Fri, 22 Nov 2024 16:34:03 +0100 Subject: [PATCH 26/61] add version to setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index eb72dbeb8bb..1cd863c5775 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ install_requires = Pillow==10.4.0 lxml==5.2.1 psycopg2==2.9.9 - Django==4.2.9 + Django==4.2.16 # Other amqp==5.2.0 From 0b792cf7d575684050469c382c57636bb7d41b22 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:05:49 +0100 Subject: [PATCH 27/61] =?UTF-8?q?[Fixes=20#12732]=20Asset=20file=20migrati?= =?UTF-8?q?on=20for=20documents=20does=20not=20work=20for=20d=E2=80=A6=20(?= =?UTF-8?q?#12733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Fixes #12732] Asset file migration for documents does not work for document * [Fixes #12732] Asset file migration for documents does not work for document --- .../commands/migrate_file_to_assets.py | 21 ++++++++++++--- ...2_migrate and_remove_resourcebase_files.py | 27 +++++++++---------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/geonode/assets/management/commands/migrate_file_to_assets.py b/geonode/assets/management/commands/migrate_file_to_assets.py index 1b3297c18f0..ab75c7ca161 100644 --- a/geonode/assets/management/commands/migrate_file_to_assets.py +++ b/geonode/assets/management/commands/migrate_file_to_assets.py @@ -98,9 +98,19 @@ def handle(self, **options): logger.info("Moving file to the asset folder") - dest = shutil.move(source, handler._create_asset_dir()) - - logger.info("Fixing perms") + if len(asset.location) == 1: + # In older installations, all documents are stored in a single folder. + # Instead of moving the entire folder, we can simply move the individual document. + # This approach prevents the risk of breaking the other documents + # that are stored in the same folder + # oldpath = {MEDIA_ROOT}/documents/document/file.extension + dest = shutil.move(asset.location[0], handler._create_asset_dir()) + else: + dest = shutil.move(source, handler._create_asset_dir()) + + logger.info(f"New destination path: {dest}") + + logger.info("Fixing file/folder perms if required") if settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS is not None: os.chmod(os.path.dirname(dest), settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS) @@ -113,7 +123,10 @@ def handle(self, **options): logger.info("Updating location field with new folder value") - asset.location = [x.replace(source, dest) for x in asset.location] + if len(asset.location) == 1: + asset.location = dest + else: + asset.location = [x.replace(source, dest) for x in asset.location] asset.save() logger.info("Checking if geoserver should be updated") diff --git a/geonode/base/migrations/0092_migrate and_remove_resourcebase_files.py b/geonode/base/migrations/0092_migrate and_remove_resourcebase_files.py index 76b564cd918..ea89c795557 100644 --- a/geonode/base/migrations/0092_migrate and_remove_resourcebase_files.py +++ b/geonode/base/migrations/0092_migrate and_remove_resourcebase_files.py @@ -1,4 +1,5 @@ # Generated by Django 4.2.9 on 2024-03-12 11:55 +import itertools import logging import os @@ -10,7 +11,7 @@ from geonode.base.models import Link from geonode.assets.models import LocalAsset -from geonode.utils import build_absolute_uri +from geonode.utils import build_absolute_uri, get_supported_datasets_file_types logger = logging.getLogger(__name__) @@ -24,9 +25,9 @@ def get_ext(filename): logger.warning(f"Could not find extension for Resource '{res_hm.title}, file '{filename}': {e}") return None - ResourceBase_hm = apps.get_model('base', 'ResourceBase') - Dataset_hm = apps.get_model('layers', 'Dataset') - Document_hm = apps.get_model('documents', 'Document') + ResourceBase_hm = apps.get_model("base", "ResourceBase") + Dataset_hm = apps.get_model("layers", "Dataset") + Document_hm = apps.get_model("documents", "Document") if hasattr(ResourceBase_hm, "files"): # looping on available resources with files to generate the LocalAssets @@ -37,12 +38,7 @@ def get_ext(filename): files = res_hm.files # creating the local asset object - asset = LocalAsset( - title="Files", - description="Original uploaded files", - owner=owner, - location=files - ) + asset = LocalAsset(title="Files", description="Original uploaded files", owner=owner, location=files) asset.save() ### creating the association between asset and Link @@ -60,10 +56,14 @@ def get_ext(filename): ext = get_ext(files[0]) else: ext = None + supported_file_types = get_supported_datasets_file_types() for file in files: - for filetype in settings.SUPPORTED_DATASET_FILE_TYPES: + for filetype in supported_file_types: file_ext = get_ext(file) - if file_ext in filetype["ext"]: + _ext = list( + itertools.chain.from_iterable(y for y in [x["required_ext"] for x in filetype["formats"]]) + ) + if file_ext in _ext: ext = filetype["id"] break if ext: @@ -75,14 +75,13 @@ def get_ext(filename): link_type="uploaded", name="Original upload", extension=ext or "unknown", - url=url + url=url, ) class Migration(migrations.Migration): dependencies = [ - ("base", "0091_create_link_asset_alter_link_type"), ] From 993364a44c8afa1085dccd2366794789b36a2b39 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:56:26 +0100 Subject: [PATCH 28/61] Fixes #12732: destination must be a list (#12746) even for a single file, the destination must be a list not a string --- geonode/assets/management/commands/migrate_file_to_assets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geonode/assets/management/commands/migrate_file_to_assets.py b/geonode/assets/management/commands/migrate_file_to_assets.py index ab75c7ca161..0c57fd2b54b 100644 --- a/geonode/assets/management/commands/migrate_file_to_assets.py +++ b/geonode/assets/management/commands/migrate_file_to_assets.py @@ -124,7 +124,7 @@ def handle(self, **options): logger.info("Updating location field with new folder value") if len(asset.location) == 1: - asset.location = dest + asset.location = [dest] else: asset.location = [x.replace(source, dest) for x in asset.location] asset.save() From 051efddcc61037600c6cc722819572f22a5b1c80 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:39:23 +0100 Subject: [PATCH 29/61] Update setup.cfg (#12753) --- setup.cfg | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.cfg b/setup.cfg index 1cd863c5775..dd2555fcfd8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -123,6 +123,11 @@ install_requires = gn-gsimporter==2.0.4 gisdata==0.5.4 + # importer dependencies + gdal<=3.4.3 + pdok-geopackage-validator==0.8.5 + geonode-django-dynamic-model==0.4.0 + # datetimepicker widget django-bootstrap3-datetimepicker-2==2.8.3 From eb352184f5948103f680a0f33600cdf1a7fbad5c Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:20:03 +0100 Subject: [PATCH 30/61] [Fixes #12763] 3D tiles geometricError mandatory field should be on tileset level (#12764) * [Fixes #12763] 3D tiles geometricError mandatory field should be on tileset level * [Fixes #12763] 3D tiles geometricError mandatory field should be on tileset level * [Fixes #12763] 3D tiles geometricError mandatory field should be on tileset level --- geonode/upload/handlers/tiles3d/handler.py | 2 +- geonode/upload/handlers/tiles3d/tests.py | 6 ++++-- geonode/upload/tests/end2end/test_end2end.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/geonode/upload/handlers/tiles3d/handler.py b/geonode/upload/handlers/tiles3d/handler.py index 08a25443652..434b567f018 100755 --- a/geonode/upload/handlers/tiles3d/handler.py +++ b/geonode/upload/handlers/tiles3d/handler.py @@ -138,7 +138,7 @@ def validate_3dtile_payload(payload): if not volume: raise Invalid3DTilesException("The mandatory 'boundingVolume' for the key 'root' is missing") - error = payload.get("root", {}).get("geometricError", None) + error = payload.get("geometricError", None) or payload.get("root", {}).get("geometricError", None) if error is None: raise Invalid3DTilesException("The mandatory 'geometricError' for the key 'root' is missing") diff --git a/geonode/upload/handlers/tiles3d/tests.py b/geonode/upload/handlers/tiles3d/tests.py index 6aa3982d3e2..7ba1696f794 100755 --- a/geonode/upload/handlers/tiles3d/tests.py +++ b/geonode/upload/handlers/tiles3d/tests.py @@ -154,7 +154,6 @@ def test_validate_should_raise_exception_for_invalid_root_boundingVolume(self): def test_validate_should_raise_exception_for_invalid_root_geometricError(self): _json = { "asset": {"version": "1.1"}, - "geometricError": 1.0, "root": {"boundingVolume": {"box": []}, "foo": 0.0}, } _path = "/tmp/tileset.json" @@ -164,7 +163,10 @@ def test_validate_should_raise_exception_for_invalid_root_geometricError(self): self.handler.is_valid(files={"base_file": _path}, user=self.user) self.assertIsNotNone(_exc) - self.assertTrue("The mandatory 'geometricError' for the key 'root' is missing" in str(_exc.exception.detail)) + self.assertTrue( + "The provided 3DTiles is not valid, some of the mandatory keys are missing. Mandatory keys are: 'asset', 'geometricError', 'root'" + in str(_exc.exception.detail) + ) os.remove(_path) def test_get_ogr2ogr_driver_should_return_the_expected_driver(self): diff --git a/geonode/upload/tests/end2end/test_end2end.py b/geonode/upload/tests/end2end/test_end2end.py index f0a493b144e..176259b7199 100644 --- a/geonode/upload/tests/end2end/test_end2end.py +++ b/geonode/upload/tests/end2end/test_end2end.py @@ -457,7 +457,7 @@ def test_import_wms(self): "parse_remote_metadata": True, "action": "upload", } - initial_name = res.title + initial_name = res.title.lower().replace(" ", "_") assert_payload = { "subtype": "remote", "title": res.title, From 91f32c64e8d8e6f159368243d8dfac96fe5a8aca Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:50:40 +0100 Subject: [PATCH 31/61] [Fixes #12770] Thesaurus title label is not rendered (#12771) --- geonode/base/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 7f83bb4d418..453e1ebbdc4 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -342,6 +342,7 @@ def _get_thesauro_keyword_label(item, lang): @staticmethod def _get_thesauro_title_label(item, lang): + lang = remove_country_from_languagecode(lang) tname = ThesaurusLabel.objects.values_list("label", flat=True).filter(thesaurus=item).filter(lang=lang) if not tname: return Thesaurus.objects.get(id=item.id).title From cb3d2543cfdb902725176d98943c130412b32b74 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:18:24 +0100 Subject: [PATCH 32/61] FILE_UPLOAD_HANDLERS change handlers order (#12817) --- geonode/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geonode/settings.py b/geonode/settings.py index 2be775af909..fc854a583b4 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -2172,8 +2172,8 @@ def get_geonode_catalogue_service(): FILE_UPLOAD_HANDLERS = [ "geonode.upload.uploadhandler.SizeRestrictedFileUploadHandler", - "django.core.files.uploadhandler.MemoryFileUploadHandler", "django.core.files.uploadhandler.TemporaryFileUploadHandler", + "django.core.files.uploadhandler.MemoryFileUploadHandler", ] DEFAULT_MAX_UPLOAD_SIZE = int(os.getenv("DEFAULT_MAX_UPLOAD_SIZE", 104857600)) # 100 MB From e28ac9399731f3acbe8f9b39848e9d46605600d7 Mon Sep 17 00:00:00 2001 From: Emanuele Tajariol Date: Wed, 15 Jan 2025 17:26:40 +0100 Subject: [PATCH 33/61] GNIP 97: New metadata editor (#12794) * Initial commit for the metadata architecture refactoring * improving the code * update the first handler * rename the file of the main schema * fixing manage.py * For testing a specific folder for json schemas examples was created * formatting the json schema files * update the json schema examples * adding the metadata/schema endpoint under api/v2 * rename the action of getting schema * adding the metadata/instance/{pk} endpoint * adding handlers registry * update metadata manager * update the metadata/{pk} to metadata/instance/{pk} * update the /metadata/schema endpoint * Handlers refactoring, i18n * Add TKeywords subschema * Metadata TKeywords: fix max card * TKeywords: Fix schema * Tkeywords: void get_jsonschema_instance * TKeywords: Fix autocomplete; localization * Thesaurus schema: Improve localization * TKeywords: Improve autocomplete * adding PUT functionality to the endpoint metadata/instance/{pk} * rename the view of metadata/instance/{pk} endpoint * TKeywords: Improve autocomplete * TKeywords: move tkeywords just under category field * Many improvements and addings to the base handler * Some more improvements and addings to the base handler * Return proper json schema instance * Return proper json schema instance * adding a handler for the regions field: RegionsHandler * Add DOI handler * Improvements and fixes * fixing Region autocomplete * Add DOI handler * Simplify tkeywords schema * adding serialize method to other FKs of the BaseHandler * Extending PUT and removing serialization * Fix PUT/PATCH * Fixes: now patch returns without major errors * Storing FKs to the resource model * small improvements to store FK values * TKeywords get and patch working. Added i18n to instance request * Cleanup: black and flake * Added contacts schema. Moved tkeywords autocomplete. * Load+store contacts * Added linked resources handler * Regions autocomplete * Regions load/store * Extending the Regions autocomplete results * format fixing * update the MetadataRegionsAutocomplete class * Metadata: review label i18n * Metadata: hkeywords handler - WIP * Minor improvement * Metadata: hkeywords handler * Metadata: group handler * Metadata: set owner fields as required * Metadata: doi: implement update_resource * Many improvements and fixes Sparse fields, model + handler Fix id type Handling required fields Add load_serialization_context Add null type to most optional fields Caching schema Simplified handler registration * Cleanup * Add error handling, Improve sparse field loading * Initial INSPIRE app * May improvements: sparse fields, i18n,... - Handling complex values in sparse fields - Added i18n via thesaurus - Improved subschema handling - Renamed base schema json file * tests for views * adding more tests for views * Tkeywords: hide property if no thesaurus configured * Create test errors recursively * Recurse localization in complex sparse fields * Metadata: fix contact roles * Metadata: improve handling of None values in sparse fields * Metadata: add authorization to metadata access * Metadata: fix required rolenames * Metadata: improve type handling in sparse fields * adding tests for views and manager * Metadata: improve handling of None values in sparse fields * Metadata: tentative handling of categories via autocomplete * Metadata: tentative handling of categories via autocomplete * adding base handlers tests * Metadata: tentative handling of categories via autocomplete * Metadata: handling licenses via autocomplete * Black/flake * Fix i18n caching * adding more tests for the BaseHandler * Fix flake * Black/flake * Metadata: fix group handling * Metadata: fix FK handling * adding tests for region and linkedrsources handers * fixing tests * black reformating * adding tests and reformatting * removing unused modules * removing Permissions module * adding tests for Group and Hkeyword handlers * add a flake issue * fixing views tsts * adding tests for Contact and Thesaurus handlers and autocomplete views * formatting issues * adding tests for autocomplete views and Thesaurus handler * adding tests for sparse handler * fixing format issues * Fix load_thesaurus * Remove stale sample schemas * Metadata: reload schema when labels on DB change * Fix linked resources API * Added __init__ to tests dir * Fix UserHasPerms in views * Remove geonode.inspire app * Some improvements after review * Delete unneeded migration * Some improvements after review * Metadata: show contact cardinalities only in debug mode * Fix save: make proper signals work --------- Co-authored-by: gpetrak --- geonode/base/admin.py | 2 +- geonode/base/api/views.py | 24 +- .../management/commands/load_thesaurus.py | 3 +- geonode/base/urls.py | 6 - geonode/base/views.py | 26 - geonode/metadata/__init__.py | 0 geonode/metadata/admin.py | 0 geonode/metadata/api/urls.py | 72 + geonode/metadata/api/views.py | 272 +++ geonode/metadata/apps.py | 37 + geonode/metadata/exceptions.py | 10 + geonode/metadata/handlers/abstract.py | 137 ++ geonode/metadata/handlers/base.py | 225 ++ geonode/metadata/handlers/contact.py | 151 ++ geonode/metadata/handlers/doi.py | 48 + geonode/metadata/handlers/group.py | 75 + geonode/metadata/handlers/hkeyword.py | 61 + geonode/metadata/handlers/linkedresource.py | 67 + geonode/metadata/handlers/region.py | 67 + geonode/metadata/handlers/sparse.py | 193 ++ geonode/metadata/handlers/thesaurus.py | 169 ++ geonode/metadata/i18n.py | 39 + geonode/metadata/manager.py | 186 ++ geonode/metadata/migrations/0001_initial.py | 29 + geonode/metadata/migrations/__init__.py | 0 geonode/metadata/models.py | 52 + geonode/metadata/schemas/base.json | 159 ++ geonode/metadata/settings.py | 25 + geonode/metadata/signals.py | 17 + geonode/metadata/tests/__init__.py | 0 .../metadata/tests/data/fake_base_schema.json | 32 + geonode/metadata/tests/data/fake_schema.json | 26 + geonode/metadata/tests/test_handlers.py | 1837 +++++++++++++++++ geonode/metadata/tests/tests.py | 985 +++++++++ geonode/metadata/urls.py | 3 + geonode/metadata/views.py | 1 + geonode/people/__init__.py | 4 + geonode/settings.py | 1 + geonode/urls.py | 2 + 39 files changed, 5004 insertions(+), 39 deletions(-) create mode 100644 geonode/metadata/__init__.py create mode 100644 geonode/metadata/admin.py create mode 100644 geonode/metadata/api/urls.py create mode 100644 geonode/metadata/api/views.py create mode 100644 geonode/metadata/apps.py create mode 100644 geonode/metadata/exceptions.py create mode 100644 geonode/metadata/handlers/abstract.py create mode 100644 geonode/metadata/handlers/base.py create mode 100644 geonode/metadata/handlers/contact.py create mode 100644 geonode/metadata/handlers/doi.py create mode 100644 geonode/metadata/handlers/group.py create mode 100644 geonode/metadata/handlers/hkeyword.py create mode 100644 geonode/metadata/handlers/linkedresource.py create mode 100644 geonode/metadata/handlers/region.py create mode 100644 geonode/metadata/handlers/sparse.py create mode 100644 geonode/metadata/handlers/thesaurus.py create mode 100644 geonode/metadata/i18n.py create mode 100644 geonode/metadata/manager.py create mode 100644 geonode/metadata/migrations/0001_initial.py create mode 100644 geonode/metadata/migrations/__init__.py create mode 100644 geonode/metadata/models.py create mode 100644 geonode/metadata/schemas/base.json create mode 100644 geonode/metadata/settings.py create mode 100644 geonode/metadata/signals.py create mode 100644 geonode/metadata/tests/__init__.py create mode 100644 geonode/metadata/tests/data/fake_base_schema.json create mode 100644 geonode/metadata/tests/data/fake_schema.json create mode 100644 geonode/metadata/tests/test_handlers.py create mode 100644 geonode/metadata/tests/tests.py create mode 100644 geonode/metadata/urls.py create mode 100644 geonode/metadata/views.py diff --git a/geonode/base/admin.py b/geonode/base/admin.py index 0f6c4ee859c..5d8ace0529e 100755 --- a/geonode/base/admin.py +++ b/geonode/base/admin.py @@ -247,7 +247,7 @@ def import_rdf(self, request): if request.method == "POST": try: rdf_file = request.FILES["rdf_file"] - name = slugify(rdf_file.name) + name = slugify(rdf_file.name).removesuffix("-rdf") call_command("load_thesaurus", file=rdf_file, name=name) self.message_user(request, "Your RDF file has been imported", messages.SUCCESS) return redirect("..") diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index 8fbbdf71407..a7ad7df6ddb 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -1457,7 +1457,7 @@ def base_linked_resources(instance, user, params): return Response(data={"message": e.args[0], "success": False}, status=500, exception=True) -def base_linked_resources_payload(instance, user, params={}): +def base_linked_resources_instances(instance, user, params={}): resource_type = params.get("resource_type", None) link_type = params.get("link_type", None) type_list = resource_type.split(",") if resource_type else [] @@ -1498,7 +1498,7 @@ def base_linked_resources_payload(instance, user, params={}): linked_to_visib_ids = linked_to_visib.values_list("id", flat=True) linked_to = [lres for lres in linked_to_over_loopable if lres.target.id in linked_to_visib_ids] - ret["linked_to"] = LinkedResourceSerializer(linked_to, embed=True, many=True).data + ret["linked_to"] = linked_to if not link_type or link_type == "linked_by": linked_by_over = instance.get_linked_resources(as_target=True) @@ -1517,11 +1517,25 @@ def base_linked_resources_payload(instance, user, params={}): linked_by_visib_ids = linked_by_visib.values_list("id", flat=True) linked_by = [lres for lres in linked_by_over_loopable if lres.source.id in linked_by_visib_ids] - ret["linked_by"] = LinkedResourceSerializer( - instance=linked_by, serialize_source=True, embed=True, many=True - ).data + ret["linked_by"] = linked_by if not ret["WARNINGS"]: ret.pop("WARNINGS") return ret + + +def base_linked_resources_payload(instance, user, params={}): + lres = base_linked_resources_instances(instance, user, params) + ret = {} + if "linked_to" in lres: + ret["linked_to"] = LinkedResourceSerializer(lres["linked_to"], embed=True, many=True).data + if "linked_by" in lres: + ret["linked_by"] = LinkedResourceSerializer( + instance=lres["linked_by"], serialize_source=True, embed=True, many=True + ).data + + if lres.get("WARNINGS", None): + ret["WARNINGS"] = lres["WARNINGS"] + + return ret diff --git a/geonode/base/management/commands/load_thesaurus.py b/geonode/base/management/commands/load_thesaurus.py index a67a512f807..388c29f0d53 100644 --- a/geonode/base/management/commands/load_thesaurus.py +++ b/geonode/base/management/commands/load_thesaurus.py @@ -114,7 +114,8 @@ def load_thesaurus(self, input_file, name, store): thesaurus_label.save() for concept in g.subjects(RDF.type, SKOS.Concept): - pref = preferredLabel(g, concept, default_lang)[0][1] + prefs = preferredLabel(g, concept, default_lang) + pref = prefs[0][1] if prefs else "-" about = str(concept) alt_label = g.value(concept, SKOS.altLabel, object=None, default=None) if alt_label is not None: diff --git a/geonode/base/urls.py b/geonode/base/urls.py index 28e2b37aacf..cc0572f19eb 100644 --- a/geonode/base/urls.py +++ b/geonode/base/urls.py @@ -26,7 +26,6 @@ OwnerRightsRequestView, ResourceBaseAutocomplete, HierarchicalKeywordAutocomplete, - ThesaurusKeywordLabelAutocomplete, LinkedResourcesAutocomplete, ) @@ -57,11 +56,6 @@ ThesaurusAvailable.as_view(), name="thesaurus_available", ), - re_path( - r"^thesaurus_autocomplete/$", - ThesaurusKeywordLabelAutocomplete.as_view(), - name="thesaurus_autocomplete", - ), re_path( r"^datasets_autocomplete/$", DatasetsAutocomplete.as_view(), diff --git a/geonode/base/views.py b/geonode/base/views.py index 31f4bf2f677..b08ca239a68 100644 --- a/geonode/base/views.py +++ b/geonode/base/views.py @@ -334,32 +334,6 @@ class HierarchicalKeywordAutocomplete(SimpleSelect2View): filter_arg = "slug__icontains" -class ThesaurusKeywordLabelAutocomplete(autocomplete.Select2QuerySetView): - def get_queryset(self): - thesaurus = settings.THESAURUS - tname = thesaurus["name"] - lang = "en" - - # Filters thesaurus results based on thesaurus name and language - qs = ThesaurusKeywordLabel.objects.all().filter(keyword__thesaurus__identifier=tname, lang=lang) - - if self.q: - qs = qs.filter(label__icontains=self.q) - - return qs - - # Overides the get results method to return custom json to frontend - def get_results(self, context): - return [ - { - "id": self.get_result_value(result.keyword), - "text": self.get_result_label(result), - "selected_text": self.get_selected_result_label(result), - } - for result in context["object_list"] - ] - - class DatasetsAutocomplete(SimpleSelect2View): model = Dataset filter_arg = "title__icontains" diff --git a/geonode/metadata/__init__.py b/geonode/metadata/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/metadata/admin.py b/geonode/metadata/admin.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/metadata/api/urls.py b/geonode/metadata/api/urls.py new file mode 100644 index 00000000000..39795f568ff --- /dev/null +++ b/geonode/metadata/api/urls.py @@ -0,0 +1,72 @@ +######################################################################### +# +# 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.urls import path +from rest_framework import routers + +from geonode.metadata.api import views +from geonode.metadata.api.views import ( + ProfileAutocomplete, + MetadataLinkedResourcesAutocomplete, + MetadataRegionsAutocomplete, + MetadataHKeywordAutocomplete, + MetadataGroupAutocomplete, +) + +router = routers.DefaultRouter() +router.register(r"metadata", views.MetadataViewSet, basename="metadata") + +urlpatterns = router.urls + [ + path( + r"metadata/autocomplete/thesaurus//keywords", + views.tkeywords_autocomplete, + name="metadata_autocomplete_tkeywords", + ), + path(r"metadata/autocomplete/users", ProfileAutocomplete.as_view(), name="metadata_autocomplete_users"), + path( + r"metadata/autocomplete/resources", + MetadataLinkedResourcesAutocomplete.as_view(), + name="metadata_autocomplete_resources", + ), + path( + r"metadata/autocomplete/regions", + MetadataRegionsAutocomplete.as_view(), + name="metadata_autocomplete_regions", + ), + path( + r"metadata/autocomplete/hkeywords", + MetadataHKeywordAutocomplete.as_view(), + name="metadata_autocomplete_hkeywords", + ), + path( + r"metadata/autocomplete/groups", + MetadataGroupAutocomplete.as_view(), + name="metadata_autocomplete_groups", + ), + path( + r"metadata/autocomplete/categories", + views.categories_autocomplete, + name="metadata_autocomplete_categories", + ), + path( + r"metadata/autocomplete/licenses", + views.licenses_autocomplete, + name="metadata_autocomplete_licenses", + ), + # path(r"metadata/autocomplete/users", login_required(ProfileAutocomplete.as_view()), name="metadata_autocomplete_users"), +] diff --git a/geonode/metadata/api/views.py b/geonode/metadata/api/views.py new file mode 100644 index 00000000000..67c6689073b --- /dev/null +++ b/geonode/metadata/api/views.py @@ -0,0 +1,272 @@ +######################################################################### +# +# Copyright (C) 2024 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 dal import autocomplete +from oauth2_provider.contrib.rest_framework import OAuth2Authentication +from rest_framework.authentication import BasicAuthentication, SessionAuthentication +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.viewsets import ViewSet +from rest_framework.decorators import action +from rest_framework.response import Response + +from django.contrib.auth import get_user_model +from django.core.handlers.wsgi import WSGIRequest +from django.http import JsonResponse +from django.utils.translation.trans_real import get_language_from_request +from django.utils.translation import get_language, gettext as _ +from django.db.models import Q + +from geonode.base.api.permissions import UserHasPerms +from geonode.base.models import ResourceBase, ThesaurusKeyword, ThesaurusKeywordLabel, TopicCategory, License +from geonode.base.utils import remove_country_from_languagecode +from geonode.base.views import LinkedResourcesAutocomplete, RegionAutocomplete, HierarchicalKeywordAutocomplete +from geonode.groups.models import GroupProfile +from geonode.metadata.manager import metadata_manager +from geonode.people.utils import get_available_users + +logger = logging.getLogger(__name__) + + +class MetadataViewSet(ViewSet): + authentication_classes = [SessionAuthentication, BasicAuthentication, OAuth2Authentication] + permission_classes = [IsAuthenticatedOrReadOnly, UserHasPerms] + + """ + Simple viewset that return the metadata JSON schema + """ + + queryset = ResourceBase.objects.all() + + def list(self, request): + pass + + # Get the JSON schema + # A pk argument is set for futured multiple schemas + @action(detail=False, methods=["get"], url_path=r"schema(?:/(?P\d+))?", url_name="schema") + def schema(self, request, pk=None): + """ + The user is able to export her/his keys with + resource scope. + """ + + lang = request.query_params.get("lang", get_language_from_request(request)[:2]) + schema = metadata_manager.get_schema(lang) + + if schema: + return Response(schema) + + else: + response = {"Message": "Schema not found"} + return Response(response) + + # Handle the JSON schema instance + @action( + detail=False, + methods=["get", "put"], + url_path=r"instance/(?P\d+)", + url_name="schema_instance", + permission_classes=[ + UserHasPerms( + perms_dict={ + "default": { + "GET": ["base.view_resourcebase"], + "PUT": ["change_resourcebase_metadata"], + } + } + ) + ], + ) + def schema_instance(self, request, pk=None): + try: + resource = ResourceBase.objects.get(pk=pk) + + if request.method == "GET": + lang = request.query_params.get("lang", get_language_from_request(request)[:2]) + schema_instance = metadata_manager.build_schema_instance(resource, lang) + return JsonResponse( + schema_instance, content_type="application/schema-instance+json", json_dumps_params={"indent": 3} + ) + + elif request.method in ("PUT"): + logger.debug(f"handling request {request.method}") + # try: + # logger.debug(f"handling content {json.dumps(request.data, indent=3)}") + # except Exception as e: + # logger.warning(f"Can't parse JSON {request.data}: {e}") + errors = metadata_manager.update_schema_instance(resource, request.data) + + response = { + "message": ( + "Some errors were found while updating the resource" + if errors + else "The resource was updated successfully" + ), + "extraErrors": errors, + } + + return Response(response, status=400 if errors else 200) + + except ResourceBase.DoesNotExist: + result = {"message": "The dataset was not found"} + return Response(result, status=404) + + +def tkeywords_autocomplete(request: WSGIRequest, thesaurusid): + + lang = remove_country_from_languagecode(get_language()) + all_keywords_qs = ThesaurusKeyword.objects.filter(thesaurus_id=thesaurusid) + + # try find results found for given language e.g. (en-us) if no results found remove country code from language to (en) and try again + localized_k_ids_qs = ThesaurusKeywordLabel.objects.filter(lang=lang, keyword_id__in=all_keywords_qs).values( + "keyword_id" + ) + + # consider all the keywords that do not have a translation in the requested language + keywords_not_translated_qs = ( + all_keywords_qs.exclude(id__in=localized_k_ids_qs).order_by("id").distinct("id").values("id") + ) + + qs = ThesaurusKeywordLabel.objects.filter(lang=lang, keyword_id__in=all_keywords_qs).order_by("label") + # if q := request.query_params.get("q", None): + if q := request.GET.get("q", None): + qs = qs.filter(label__istartswith=q) + + ret = [] + for tkl in qs.all(): + ret.append( + { + "id": tkl.keyword.about, + "label": tkl.label, + } + ) + for tk in all_keywords_qs.filter(id__in=keywords_not_translated_qs).order_by("alt_label").all(): + ret.append( + { + "id": tk.about, + "label": f"! {tk.alt_label}", + } + ) + + return JsonResponse({"results": ret}) + + +def categories_autocomplete(request: WSGIRequest): + qs = TopicCategory.objects.order_by("gn_description") + + if q := request.GET.get("q", None): + qs = qs.filter(gn_description__istartswith=q) + + ret = [] + for record in qs.all(): + ret.append( + { + "id": record.identifier, + "label": _(record.gn_description), + } + ) + + return JsonResponse({"results": ret}) + + +def licenses_autocomplete(request: WSGIRequest): + qs = License.objects.order_by("name") + + if q := request.GET.get("q", None): + qs = qs.filter(name__istartswith=q) + + ret = [] + for record in qs.all(): + ret.append( + { + "id": record.identifier, + "label": _(record.name), + } + ) + + return JsonResponse({"results": ret}) + + +class ProfileAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + if self.request and self.request.user: + qs = get_available_users(self.request.user) + else: + qs = get_user_model().objects.none() + + if self.q: + qs = qs.filter( + Q(username__icontains=self.q) + | Q(email__icontains=self.q) + | Q(first_name__icontains=self.q) + | Q(last_name__icontains=self.q) + ) + + return qs + + def get_results(self, context): + def get_label(user): + names = [n for n in (user.first_name, user.last_name) if n] + postfix = f" ({' '.join(names)})" if names else "" + return f"{user.username}{postfix}" + + """Return data for the 'results' key of the response.""" + return [{"id": self.get_result_value(result), "label": get_label(result)} for result in context["object_list"]] + + +class MetadataLinkedResourcesAutocomplete(LinkedResourcesAutocomplete): + def get_results(self, context): + return [ + {"id": self.get_result_value(result), "label": self.get_result_label(result)} + for result in context["object_list"] + ] + + +class MetadataRegionsAutocomplete(RegionAutocomplete): + def get_results(self, context): + return [ + {"id": self.get_result_value(result), "label": self.get_result_label(result)} + for result in context["object_list"] + ] + + +class MetadataHKeywordAutocomplete(HierarchicalKeywordAutocomplete): + def get_results(self, context): + return [self.get_result_label(result) for result in context["object_list"]] + + +class MetadataGroupAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + user = self.request.user if self.request else None + + if not user: + qs = GroupProfile.objects.none() + elif user.is_superuser or user.is_staff: + qs = GroupProfile.objects.all() + else: + qs = GroupProfile.objects.filter(groupmember__user=user) + + qs = qs.order_by("title") + if self.q: + qs = qs.filter(title__icontains=self.q) + + return qs + + def get_results(self, context): + return [{"id": self.get_result_value(result), "label": result.title} for result in context["object_list"]] diff --git a/geonode/metadata/apps.py b/geonode/metadata/apps.py new file mode 100644 index 00000000000..443a0cebcff --- /dev/null +++ b/geonode/metadata/apps.py @@ -0,0 +1,37 @@ +import logging + +from django.apps import AppConfig +from django.utils.module_loading import import_string + +logger = logging.getLogger(__name__) + + +class MetadataConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "geonode.metadata" + + def ready(self): + """Finalize setup""" + run_setup_hooks() + super(MetadataConfig, self).ready() + + +def run_setup_hooks(*args, **kwargs): + setup_metadata_handlers() + + from geonode.metadata.signals import connect_signals + + connect_signals() + + +def setup_metadata_handlers(): + from geonode.metadata.manager import metadata_manager + from geonode.metadata.settings import METADATA_HANDLERS + + ids = [] + for handler_id, module_path in METADATA_HANDLERS.items(): + handler = import_string(module_path) + metadata_manager.add_handler(handler_id, handler) + ids.append(handler_id) + + logger.info(f"Metadata handlers from config: {', '.join(METADATA_HANDLERS)}") diff --git a/geonode/metadata/exceptions.py b/geonode/metadata/exceptions.py new file mode 100644 index 00000000000..1db41f0445c --- /dev/null +++ b/geonode/metadata/exceptions.py @@ -0,0 +1,10 @@ +class MetadataFieldException(Exception): + pass + + +class UnsetFieldException(MetadataFieldException): + pass + + +class UnparsableFieldException(MetadataFieldException): + pass diff --git a/geonode/metadata/handlers/abstract.py b/geonode/metadata/handlers/abstract.py new file mode 100644 index 00000000000..f47ddbe8d06 --- /dev/null +++ b/geonode/metadata/handlers/abstract.py @@ -0,0 +1,137 @@ +######################################################################### +# +# Copyright (C) 2024 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 abc import ABCMeta, abstractmethod +from typing_extensions import deprecated + +from django.utils.translation import gettext as _ + +from geonode.base.models import ResourceBase + +logger = logging.getLogger(__name__) + + +class MetadataHandler(metaclass=ABCMeta): + """ + Handlers take care of reading, storing, encoding, + decoding subschemas of the main Resource + """ + + @abstractmethod + def update_schema(self, jsonschema: dict, context, lang=None): + """ + It is called by the MetadataManager when creating the JSON Schema + It adds the subschema handled by the handler, and returns the + augmented instance of the JSON Schema. + """ + pass + + @abstractmethod + def get_jsonschema_instance( + self, resource: ResourceBase, field_name: str, context: dict, errors: dict, lang: str = None + ): + """ + Called when reading metadata, returns the instance of the sub-schema + associated with the field field_name. + """ + pass + + @abstractmethod + def update_resource( + self, resource: ResourceBase, field_name: str, json_instance: dict, context: dict, errors: dict, **kwargs + ): + """ + Called when persisting data, updates the field field_name of the resource + with the content content, where json_instance is the full JSON Schema instance, + in case the handler needs some cross related data contained in the resource. + """ + pass + + def load_serialization_context(self, resource: ResourceBase, jsonschema: dict, context: dict): + """ + Called before calls to get_jsonschema_instance in order to initialize info needed by the handler + """ + pass + + def load_deserialization_context(self, resource: ResourceBase, jsonschema: dict, context: dict): + """ + Called before calls to update_resource in order to initialize info needed by the handler + """ + pass + + def _add_subschema(self, jsonschema, property_name, subschema, after_what=None): + after_what = after_what or subschema.get("geonode:after", None) + + if not after_what: + jsonschema["properties"][property_name] = subschema + else: + ret_properties = {} + added = False + for key, val in jsonschema["properties"].items(): + ret_properties[key] = val + if key == after_what: + ret_properties[property_name] = subschema + added = True + + if not added: + logger.warning(f'Could not add "{property_name}" after "{after_what}"') + ret_properties[property_name] = subschema + + jsonschema["properties"] = ret_properties + + @deprecated("Use _add_subschema instead") + def _add_after(self, jsonschema, after_what, property_name, subschema): + ret_properties = {} + added = False + for key, val in jsonschema["properties"].items(): + ret_properties[key] = val + if key == after_what: + ret_properties[property_name] = subschema + added = True + + if not added: + logger.warning(f'Could not add "{property_name}" after "{after_what}"') + ret_properties[property_name] = subschema + + jsonschema["properties"] = ret_properties + + @staticmethod + def _set_error(errors: dict, path: list, msg: str): + logger.info(f"Reported message: {'/'.join(path)}: {msg} ") + elem = errors + for step in path: + elem = elem.setdefault(step, {}) + elem = elem.setdefault("__errors", []) + elem.append(msg) + + @staticmethod + def _localize_label(context, lang: str, text: str): + # Try localization via thesaurus: + label = context["labels"].get(text, None) + # fallback: gettext() + if not label: + label = _(text) + + return label + + @staticmethod + def _localize_subschema_label(context, subschema: dict, lang: str, annotation_name: str): + if annotation_name in subschema: + subschema[annotation_name] = MetadataHandler._localize_label(context, lang, subschema[annotation_name]) diff --git a/geonode/metadata/handlers/base.py b/geonode/metadata/handlers/base.py new file mode 100644 index 00000000000..a9ff33017d0 --- /dev/null +++ b/geonode/metadata/handlers/base.py @@ -0,0 +1,225 @@ +######################################################################### +# +# Copyright (C) 2024 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 datetime import datetime + +from rest_framework.reverse import reverse +from django.utils.translation import gettext as _ + +from geonode.base.models import TopicCategory, License, RestrictionCodeType, SpatialRepresentationType +from geonode.metadata.handlers.abstract import MetadataHandler +from geonode.metadata.settings import JSONSCHEMA_BASE +from geonode.base.enumerations import ALL_LANGUAGES, UPDATE_FREQUENCIES + + +logger = logging.getLogger(__name__) + + +class SubHandler: + @classmethod + def update_subschema(cls, subschema, lang=None): + pass + + @classmethod + def serialize(cls, db_value): + return db_value + + @classmethod + def deserialize(cls, field_value): + return field_value + + +class CategorySubHandler(SubHandler): + @classmethod + def update_subschema(cls, subschema, lang=None): + subschema["ui:options"] = { + "geonode-ui:autocomplete": reverse("metadata_autocomplete_categories"), + } + + @classmethod + def serialize(cls, db_value): + if db_value is None: + return None + elif isinstance(db_value, TopicCategory): + return {"id": db_value.identifier, "label": _(db_value.gn_description)} + else: + logger.warning(f"Category: can't decode <{type(db_value)}>'{db_value}'") + return None + + @classmethod + def deserialize(cls, field_value): + return TopicCategory.objects.get(identifier=field_value["id"]) if field_value else None + + +class DateTypeSubHandler(SubHandler): + @classmethod + def update_subschema(cls, subschema, lang=None): + subschema["oneOf"] = [{"const": i.lower(), "title": _(i)} for i in ["Creation", "Publication", "Revision"]] + subschema["default"] = "Publication" + + +class DateSubHandler(SubHandler): + @classmethod + def serialize(cls, value): + if isinstance(value, datetime): + return value.isoformat() + return value + + +class FrequencySubHandler(SubHandler): + @classmethod + def update_subschema(cls, subschema, lang=None): + subschema["oneOf"] = [{"const": key, "title": val} for key, val in dict(UPDATE_FREQUENCIES).items()] + + +class LanguageSubHandler(SubHandler): + @classmethod + def update_subschema(cls, subschema, lang=None): + subschema["oneOf"] = [{"const": key, "title": val} for key, val in dict(ALL_LANGUAGES).items()] + + +class LicenseSubHandler(SubHandler): + @classmethod + def update_subschema(cls, subschema, lang=None): + subschema["ui:options"] = { + "geonode-ui:autocomplete": reverse("metadata_autocomplete_licenses"), + } + + @classmethod + def serialize(cls, db_value): + if db_value is None: + return None + elif isinstance(db_value, License): + return {"id": db_value.identifier, "label": _(db_value.name)} + else: + logger.warning(f"License: can't decode <{type(db_value)}>'{db_value}'") + return None + + @classmethod + def deserialize(cls, field_value): + return License.objects.get(identifier=field_value["id"]) if field_value else None + + +class RestrictionsSubHandler(SubHandler): + @classmethod + def update_subschema(cls, subschema, lang=None): + subschema["oneOf"] = [ + {"const": tc.identifier, "title": tc.identifier, "description": tc.description} + for tc in RestrictionCodeType.objects.order_by("identifier") + ] + + @classmethod + def serialize(cls, db_value): + if isinstance(db_value, RestrictionCodeType): + return db_value.identifier + return db_value + + @classmethod + def deserialize(cls, field_value): + return RestrictionCodeType.objects.get(identifier=field_value) if field_value else None + + +class SpatialRepresentationTypeSubHandler(SubHandler): + @classmethod + def update_subschema(cls, subschema, lang=None): + subschema["oneOf"] = [ + {"const": tc.identifier, "title": tc.identifier, "description": tc.description} + for tc in SpatialRepresentationType.objects.order_by("identifier") + ] + + @classmethod + def serialize(cls, db_value): + if isinstance(db_value, SpatialRepresentationType): + return db_value.identifier + return db_value + + @classmethod + def deserialize(cls, field_value): + return SpatialRepresentationType.objects.get(identifier=field_value) if field_value else None + + +SUBHANDLERS = { + "category": CategorySubHandler, + "date_type": DateTypeSubHandler, + "date": DateSubHandler, + "language": LanguageSubHandler, + "license": LicenseSubHandler, + "maintenance_frequency": FrequencySubHandler, + "restriction_code_type": RestrictionsSubHandler, + "spatial_representation_type": SpatialRepresentationTypeSubHandler, +} + + +class BaseHandler(MetadataHandler): + """ + The base handler builds a valid empty schema with the simple + fields of the ResourceBase model + """ + + def __init__(self): + self.json_base_schema = JSONSCHEMA_BASE + self.base_schema = None + + def update_schema(self, jsonschema, context, lang=None): + + with open(self.json_base_schema) as f: + self.base_schema = json.load(f) + # building the full base schema + for property_name, subschema in self.base_schema.items(): + self._localize_subschema_label(context, subschema, lang, "title") + self._localize_subschema_label(context, subschema, lang, "description") + + jsonschema["properties"][property_name] = subschema + + # add the base handler info to the dictionary if it doesn't exist + if "geonode:handler" not in subschema: + subschema.update({"geonode:handler": "base"}) + + # perform further specific initializations + if property_name in SUBHANDLERS: + # logger.debug(f"Running subhandler for base field {property_name}") + SUBHANDLERS[property_name].update_subschema(subschema, lang) + + return jsonschema + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + field_value = getattr(resource, field_name) + + # perform specific transformation if any + if field_name in SUBHANDLERS: + # logger.debug(f"Serializing base field {field_name}") + field_value = SUBHANDLERS[field_name].serialize(field_value) + + return field_value + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + field_value = json_instance.get(field_name, None) + + try: + if field_name in SUBHANDLERS: + logger.debug(f"Deserializing base field {field_name}") + # Deserialize field values before setting them to the ResourceBase + field_value = SUBHANDLERS[field_name].deserialize(field_value) + + setattr(resource, field_name, field_value) + except Exception as e: + logger.warning(f"Error setting field {field_name}={field_value}: {e}") + self._set_error(errors, [field_name], "Error while storing field. Contact your administrator") diff --git a/geonode/metadata/handlers/contact.py b/geonode/metadata/handlers/contact.py new file mode 100644 index 00000000000..9261008bab1 --- /dev/null +++ b/geonode/metadata/handlers/contact.py @@ -0,0 +1,151 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.reverse import reverse + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.utils.translation import gettext as _ + +from geonode.metadata.handlers.abstract import MetadataHandler +from geonode.people import Roles + +logger = logging.getLogger(__name__) + +# contact roles names are spread in the code, let's map them here: +ROLE_NAMES_MAP = { + Roles.OWNER: "owner", # this is not saved as a contact + Roles.METADATA_AUTHOR: "author", + Roles.PROCESSOR: Roles.PROCESSOR.name, + Roles.PUBLISHER: Roles.PUBLISHER.name, + Roles.CUSTODIAN: Roles.CUSTODIAN.name, + Roles.POC: "pointOfContact", + Roles.DISTRIBUTOR: Roles.DISTRIBUTOR.name, + Roles.RESOURCE_USER: Roles.RESOURCE_USER.name, + Roles.RESOURCE_PROVIDER: Roles.RESOURCE_PROVIDER.name, + Roles.ORIGINATOR: Roles.ORIGINATOR.name, + Roles.PRINCIPAL_INVESTIGATOR: Roles.PRINCIPAL_INVESTIGATOR.name, +} + +NAMES_ROLE_MAP = {v: k for k, v in ROLE_NAMES_MAP.items()} + + +class ContactHandler(MetadataHandler): + """ + Handles role contacts + """ + + def update_schema(self, jsonschema, context, lang=None): + contacts = {} + required = [] + for role in Roles: + rolename = ROLE_NAMES_MAP[role] + minitems = 1 if role.is_required else 0 + card = f' [{minitems}..{"N" if role.is_multivalue else "1"}]' if settings.DEBUG else "" + if role.is_required: + required.append(rolename) + + if role.is_multivalue: + contact = { + "type": "array", + "title": self._localize_label(context, lang, role.label) + card, + "minItems": minitems, + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": _("User id"), + }, + "label": { + "type": "string", + "title": _("User name"), + }, + }, + }, + "ui:options": {"geonode-ui:autocomplete": reverse("metadata_autocomplete_users")}, + } + else: + contact = { + "type": "object", + "title": self._localize_label(context, lang, role.label) + card, + "properties": { + "id": { + "type": "string", + "title": _("User id"), + "ui:widget": "hidden", + }, + "label": { + "type": "string", + "title": _("User name"), + }, + }, + "ui:options": {"geonode-ui:autocomplete": reverse("metadata_autocomplete_users")}, + "required": ["id"] if role.is_required else [], + } + + contacts[rolename] = contact + + jsonschema["properties"]["contacts"] = { + "type": "object", + "title": self._localize_label(context, lang, "contacts"), + "properties": contacts, + "required": required, + "geonode:required": bool(required), + "geonode:handler": "contact", + } + + return jsonschema + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + def __create_user_entry(user): + names = [n for n in (user.first_name, user.last_name) if n] + postfix = f" ({' '.join(names)})" if names else "" + return {"id": str(user.id), "label": f"{user.username}{postfix}"} + + contacts = {} + for role in Roles: + rolename = ROLE_NAMES_MAP[role] + if role.is_multivalue: + content = [__create_user_entry(user) for user in resource.__get_contact_role_elements__(rolename) or []] + else: + users = resource.__get_contact_role_elements__(rolename) + if not users and role == Roles.OWNER: + users = [resource.owner] + content = __create_user_entry(users[0]) if users else None + + contacts[rolename] = content + + return contacts + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + data = json_instance[field_name] + logger.debug(f"CONTACTS {data}") + for rolename, users in data.items(): + if rolename == Roles.OWNER.name: + if not users: + logger.warning(f"User not specified for role '{rolename}'") + self._set_error(errors, ["contacts", rolename], f"User not specified for role '{rolename}'") + else: + resource.owner = get_user_model().objects.get(pk=users["id"]) + else: + ids = [u["id"] for u in users] + profiles = get_user_model().objects.filter(pk__in=ids) + resource.__set_contact_role_element__(profiles, rolename) diff --git a/geonode/metadata/handlers/doi.py b/geonode/metadata/handlers/doi.py new file mode 100644 index 00000000000..5fb565fe47e --- /dev/null +++ b/geonode/metadata/handlers/doi.py @@ -0,0 +1,48 @@ +######################################################################### +# +# Copyright (C) 2024 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.utils.translation import gettext as _ + +from geonode.metadata.handlers.abstract import MetadataHandler + +logger = logging.getLogger(__name__) + + +class DOIHandler(MetadataHandler): + + def update_schema(self, jsonschema, context, lang=None): + doi_schema = { + "type": ["string", "null"], + "title": "DOI", + "description": _("a DOI will be added by Admin before publication."), + "maxLength": 255, + "geonode:handler": "doi", + } + + # add DOI after edition + self._add_subschema(jsonschema, "doi", doi_schema, after_what="edition") + return jsonschema + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + return resource.doi + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + resource.doi = json_instance[field_name] diff --git a/geonode/metadata/handlers/group.py b/geonode/metadata/handlers/group.py new file mode 100644 index 00000000000..4e089ffdc31 --- /dev/null +++ b/geonode/metadata/handlers/group.py @@ -0,0 +1,75 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.reverse import reverse + +from django.utils.translation import gettext as _ + +from geonode.groups.models import GroupProfile +from geonode.metadata.handlers.abstract import MetadataHandler + +logger = logging.getLogger(__name__) + + +class GroupHandler(MetadataHandler): + """ + The GroupHandler handles the group FK field. + This handler is only used in the first transition to the new metadata editor, and will be then replaced by + an entry in the resource management panel + """ + + def update_schema(self, jsonschema, context, lang=None): + group_schema = { + "type": "object", + "title": _("group"), + "properties": { + "id": { + "type": "string", + "ui:widget": "hidden", + }, + "label": { + "type": "string", + "title": _("group"), + }, + }, + "geonode:handler": "group", + "ui:options": {"geonode-ui:autocomplete": reverse("metadata_autocomplete_groups")}, + } + + # add group after date_type + self._add_subschema(jsonschema, "group", group_schema, after_what="date_type") + + return jsonschema + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + return ( + {"id": str(resource.group.groupprofile.pk), "label": resource.group.groupprofile.title} + if resource.group + else None + ) + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + data = json_instance.get(field_name, None) + id = data.get("id", None) if data else None + if id is not None: + gp = GroupProfile.objects.get(pk=id) + resource.group = gp.group + else: + resource.group = None diff --git a/geonode/metadata/handlers/hkeyword.py b/geonode/metadata/handlers/hkeyword.py new file mode 100644 index 00000000000..24b67c86903 --- /dev/null +++ b/geonode/metadata/handlers/hkeyword.py @@ -0,0 +1,61 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.reverse import reverse + +from django.utils.translation import gettext as _ + +from geonode.metadata.handlers.abstract import MetadataHandler +from geonode.resource.utils import KeywordHandler + +logger = logging.getLogger(__name__) + + +class HKeywordHandler(MetadataHandler): + + def update_schema(self, jsonschema, context, lang=None): + hkeywords = { + "type": "array", + "title": _("Keywords"), + "description": _("Hierarchical keywords"), + "items": { + "type": "string", + }, + "ui:options": { + "geonode-ui:autocomplete": { + "url": reverse("metadata_autocomplete_hkeywords"), + "creatable": True, + }, + }, + "geonode:handler": "hkeyword", + } + + self._add_subschema(jsonschema, "hkeywords", hkeywords, after_what="tkeywords") + return jsonschema + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + return [keyword.name for keyword in resource.keywords.all()] + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + # TODO: see also resourcebase_form.disable_keywords_widget_for_non_superuser(request.user) + hkeywords = json_instance["hkeywords"] + cleaned = [k for k in hkeywords if k] + logger.debug(f"hkeywords: {hkeywords} --> {cleaned}") + KeywordHandler(resource, cleaned).set_keywords() diff --git a/geonode/metadata/handlers/linkedresource.py b/geonode/metadata/handlers/linkedresource.py new file mode 100644 index 00000000000..0099e08f15e --- /dev/null +++ b/geonode/metadata/handlers/linkedresource.py @@ -0,0 +1,67 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.reverse import reverse + +from django.utils.translation import gettext as _ + +from geonode.base.models import ResourceBase, LinkedResource +from geonode.metadata.handlers.abstract import MetadataHandler + +logger = logging.getLogger(__name__) + + +class LinkedResourceHandler(MetadataHandler): + + def update_schema(self, jsonschema, context, lang=None): + linked = { + "type": "array", + "title": _("Related resources"), + "description": _("Resources related to this one"), + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + }, + "label": {"type": "string", "title": _("title")}, + }, + }, + "geonode:handler": "linkedresource", + "ui:options": {"geonode-ui:autocomplete": reverse("metadata_autocomplete_resources")}, + } + + jsonschema["properties"]["linkedresources"] = linked + return jsonschema + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + return [{"id": str(lr.target.id), "label": lr.target.title} for lr in resource.get_linked_resources()] + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + data = json_instance[field_name] + new_ids = {item["id"] for item in data} + + # add requested links + for res_id in new_ids: + target = ResourceBase.objects.get(pk=res_id) + LinkedResource.objects.get_or_create(source=resource, target=target, internal=False) + + # delete remaining links + LinkedResource.objects.filter(source_id=resource.id, internal=False).exclude(target_id__in=new_ids).delete() diff --git a/geonode/metadata/handlers/region.py b/geonode/metadata/handlers/region.py new file mode 100644 index 00000000000..e6690cb7e00 --- /dev/null +++ b/geonode/metadata/handlers/region.py @@ -0,0 +1,67 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.reverse import reverse + +from django.utils.translation import gettext as _ + +from geonode.base.models import Region +from geonode.metadata.handlers.abstract import MetadataHandler + +logger = logging.getLogger(__name__) + + +class RegionHandler(MetadataHandler): + """ + The RegionsHandler adds the Regions model options to the schema + """ + + def update_schema(self, jsonschema, context, lang=None): + regions = { + "type": "array", + "title": _("Regions"), + "description": _("keyword identifies a location"), + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + }, + "label": {"type": "string", "title": _("title")}, + }, + }, + "geonode:handler": "region", + "ui:options": {"geonode-ui:autocomplete": reverse("metadata_autocomplete_regions")}, + } + + # add regions after Attribution + self._add_subschema(jsonschema, "regions", regions, after_what="attribution") + return jsonschema + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + return [{"id": str(r.id), "label": r.name} for r in resource.regions.all()] + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + data = json_instance[field_name] + new_ids = {item["id"] for item in data} + logger.info(f"Regions added {data} --> {new_ids}") + + regions = Region.objects.filter(id__in=new_ids) + resource.regions.set(regions) diff --git a/geonode/metadata/handlers/sparse.py b/geonode/metadata/handlers/sparse.py new file mode 100644 index 00000000000..b62b0b0a796 --- /dev/null +++ b/geonode/metadata/handlers/sparse.py @@ -0,0 +1,193 @@ +######################################################################### +# +# Copyright (C) 2024 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 copy +import json +import logging + +from geonode.metadata.handlers.abstract import MetadataHandler +from geonode.metadata.exceptions import UnsetFieldException +from geonode.metadata.models import SparseField + +logger = logging.getLogger(__name__) + + +CONTEXT_ID = "sparse" + + +class SparseFieldRegistry: + + sparse_fields = {} + + def register(self, field_name: str, schema: dict, after: str = None, init_func=None): + self.sparse_fields[field_name] = {"schema": schema, "after": after, "init_func": init_func} + + def fields(self): + return self.sparse_fields + + +sparse_field_registry = SparseFieldRegistry() + + +class SparseHandler(MetadataHandler): + """ + Handles sparse in fields in the SparseField table + """ + + def _recurse_localization(self, context, schema, lang): + self._localize_subschema_label(context, schema, lang, "title") + self._localize_subschema_label(context, schema, lang, "description") + + match schema["type"]: + case "object": + for subschema in schema["properties"].values(): + self._recurse_localization(context, subschema, lang) + case "array": + self._recurse_localization(context, schema["items"], lang) + case _: + pass + + def update_schema(self, jsonschema, context, lang=None): + # add all registered fields + for field_name, field_info in sparse_field_registry.fields().items(): + subschema = copy.deepcopy(field_info["schema"]) + self._recurse_localization(context, subschema, lang) + self._add_subschema(jsonschema, field_name, subschema, after_what=field_info["after"]) + + # add the handler info to the dictionary if it doesn't exist + if "geonode:handler" not in subschema: + subschema.update({"geonode:handler": "sparse"}) + + # # perform further specific initializations + # if init_func := field_info["init_func"]: + # logger.debug(f"Running init for sparse field {field_name}") + # init_func(field_name, subschema, lang) + + return jsonschema + + def load_serialization_context(self, resource, jsonschema: dict, context: dict): + logger.debug(f"Preloading sparse fields {sparse_field_registry.fields().keys()}") + context[CONTEXT_ID] = { + "fields": { + f.name: f.value for f in SparseField.get_fields(resource, names=sparse_field_registry.fields().keys()) + }, + "schema": jsonschema, + } + + @staticmethod + def _check_type(declared, checked): + return declared == checked or (type(declared) is list and checked in declared) + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + field_type = context[CONTEXT_ID]["schema"]["properties"][field_name]["type"] + field_value = context[CONTEXT_ID]["fields"].get(field_name, None) + + is_nullable = self._check_type(field_type, "null") + + if field_name not in context[CONTEXT_ID]["fields"] and not is_nullable: + raise UnsetFieldException() + + if is_nullable and field_value is None: + return None + + if self._check_type(field_type, "string"): + return field_value + elif self._check_type(field_type, "number"): + try: + return float(field_value) + except Exception as e: + logger.warning( + f"Error loading NUMBER field '{field_name}' with content ({type(field_value)}){field_value}: {e}" + ) + raise UnsetFieldException() # should be a different exception + elif self._check_type(field_type, "integer"): + try: + return int(field_value) + except Exception as e: + logger.warning( + f"Error loading INTEGER field '{field_name}' with content ({type(field_value)}){field_value}: {e}" + ) + raise UnsetFieldException() # should be a different exception + elif field_type == "array": + # assuming it's a single level array: TODO implement other cases + try: + return json.loads(field_value) if field_value is not None else None + except Exception as e: + logger.warning( + f"Error loading field '{field_name}' with content ({type(field_value)}){field_value}: {e}" + ) + elif field_type == "object": + # assuming it's a single level object: TODO implement other cases + try: + return json.loads(field_value) if field_value is not None else None + except Exception as e: + logger.warning( + f"Error loading field '{field_name}' with content ({type(field_value)}){field_value}: {e}" + ) + else: + logger.warning(f"Unhandled type '{field_type}' for sparse field '{field_name}'") + return None + + def load_deserialization_context(self, resource, jsonschema: dict, context: dict): + context[CONTEXT_ID] = {"schema": jsonschema} + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + + bare_value = json_instance.get(field_name, None) + field_type = context[CONTEXT_ID]["schema"]["properties"][field_name]["type"] + + is_nullable = self._check_type(field_type, "null") + + if self._check_type(field_type, "string"): + field_value = bare_value + elif self._check_type(field_type, "number"): + try: + field_value = str(float(bare_value)) if bare_value is not None else None + except ValueError as e: + logger.warning(f"Error parsing sparse field '{field_name}'::'{field_type}'='{bare_value}': {e}") + self._set_error(errors, [field_name], f"Error parsing number '{bare_value}'") + return + elif self._check_type(field_type, "integer"): + try: + field_value = str(int(bare_value)) if bare_value is not None else None + except ValueError as e: + logger.warning(f"Error parsing sparse field '{field_name}'::'{field_type}'='{bare_value}': {e}") + self._set_error(errors, [field_name], f"Error parsing integer '{bare_value}'") + return + elif field_type == "array": + field_value = json.dumps(bare_value) if bare_value is not None else "[]" + elif field_type == "object": + field_value = json.dumps(bare_value) if bare_value is not None else "{}" + else: + logger.warning(f"Unhandled type '{field_type}' for sparse field '{field_name}'") + self._set_error(errors, [field_name], f"Unhandled type {field_type}. Contact your administrator") + return + + try: + if field_value is not None: + SparseField.objects.update_or_create( + defaults={"value": field_value}, resource=resource, name=field_name + ) + elif is_nullable: + SparseField.objects.filter(resource=resource, name=field_name).delete() + else: + self._set_error(errors, [field_name], f"Empty value not stored for field '{field_name}'") + logger.debug(f"Not setting null value for {field_name}") + except Exception as e: + logger.warning(f"Error setting field {field_name}={field_value}: {e}") + self._set_error(errors, [field_name], f"Error setting value: {e}") diff --git a/geonode/metadata/handlers/thesaurus.py b/geonode/metadata/handlers/thesaurus.py new file mode 100644 index 00000000000..3bb5f9093d7 --- /dev/null +++ b/geonode/metadata/handlers/thesaurus.py @@ -0,0 +1,169 @@ +######################################################################### +# +# Copyright (C) 2024 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 rest_framework.reverse import reverse + +from django.db.models import Q +from django.utils.translation import gettext as _ + +from geonode.base.models import Thesaurus, ThesaurusKeyword, ThesaurusKeywordLabel +from geonode.metadata.handlers.abstract import MetadataHandler + + +logger = logging.getLogger(__name__) + + +TKEYWORDS = "tkeywords" + + +class TKeywordsHandler(MetadataHandler): + """ + Handles the keywords for all the Thesauri with max card > 0 + """ + + @staticmethod + def collect_thesauri(filter, lang=None): + # this query return the list of thesaurus X the list of localized titles + q = ( + Thesaurus.objects.filter(filter) + .values( + "id", + "identifier", + "title", + "description", + "order", + "card_min", + "card_max", + "rel_thesaurus__label", + "rel_thesaurus__lang", + ) + .order_by("order") + ) + + # We don't know if we have the title for the requested lang: so let's loop on all retrieved translations + collected_thesauri = {} + for r in q.all(): + identifier = r["identifier"] + thesaurus = collected_thesauri.get(identifier, {}) + if not thesaurus: + # init + logger.debug(f"Initializing Thesaurus {lang}/{identifier} JSON Schema") + collected_thesauri[identifier] = thesaurus + thesaurus["id"] = r["id"] + thesaurus["card"] = {} + thesaurus["card"]["minItems"] = r["card_min"] + if r["card_max"] != -1: + thesaurus["card"]["maxItems"] = r["card_max"] + thesaurus["title"] = r["title"] # default title + thesaurus["description"] = r["description"] # not localized in db + + # check if this is the localized record we're looking for + if r["rel_thesaurus__lang"] == lang: + logger.debug(f"Localizing Thesaurus {identifier} JSON Schema for lang {lang}") + thesaurus["title"] = r["rel_thesaurus__label"] + + return collected_thesauri + + def update_schema(self, jsonschema, context, lang=None): + + collected_thesauri = self.collect_thesauri(~Q(card_max=0), lang=lang) + + # copy info to json schema + thesauri = {} + for id, ct in collected_thesauri.items(): + thesaurus = { + "type": "array", + "title": ct["title"], + "description": ct["description"], + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "keyword id", + "description": "The id of the keyword (usually a URI)", + }, + "label": { + "type": "string", + "title": "Label", + "description": "localized label for the keyword", + }, + }, + }, + "ui:options": { + "geonode-ui:autocomplete": reverse( + "metadata_autocomplete_tkeywords", kwargs={"thesaurusid": ct["id"]} + ) + }, + } + + thesaurus.update(ct["card"]) + thesauri[id] = thesaurus + + tkeywords = { + "type": "object", + "title": _("Keywords from Thesaurus"), + "description": _("List of keywords from Thesaurus"), + "geonode:handler": "thesaurus", + "properties": thesauri, + } + + # We are going to hide the tkeywords property if there's no thesaurus configured + # We can't remove the property altogether, since hkeywords relies on tkeywords for positioning + if not thesauri: + tkeywords["ui:widget"] = "hidden" + + # add thesauri after category + self._add_subschema(jsonschema, TKEYWORDS, tkeywords, after_what="category") + + return jsonschema + + def get_jsonschema_instance(self, resource, field_name, context, errors, lang=None): + tks = {} + for tk in resource.tkeywords.all(): + tks[tk.id] = tk + tkls = ThesaurusKeywordLabel.objects.filter( + keyword__id__in=tks.keys(), lang=lang + ) # read all entries in a single query + + ret = {} + for tkl in tkls: + keywords = ret.setdefault(tkl.keyword.thesaurus.identifier, []) + keywords.append({"id": tkl.keyword.about, "label": tkl.label}) + del tks[tkl.keyword.id] + + if tks: + logger.info(f"Returning untranslated '{lang}' keywords: {tks}") + for tk in tks.values(): + keywords = ret.setdefault(tk.thesaurus.identifier, []) + keywords.append({"id": tk.about, "label": tk.alt_label}) + + return ret + + def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): + kids = [] + for thes_id, keywords in json_instance.get(TKEYWORDS, {}).items(): + logger.info(f"Getting info for thesaurus {thes_id}") + for keyword in keywords: + kids.append(keyword["id"]) + + kw_requested = ThesaurusKeyword.objects.filter(about__in=kids) + resource.tkeywords.set(kw_requested) diff --git a/geonode/metadata/i18n.py b/geonode/metadata/i18n.py new file mode 100644 index 00000000000..884359630fe --- /dev/null +++ b/geonode/metadata/i18n.py @@ -0,0 +1,39 @@ +import logging + +from django.db import connection + +logger = logging.getLogger(__name__) + +I18N_THESAURUS_IDENTIFIER = "labels-i18n" + + +def get_localized_tkeywords(lang, thesaurus_identifier: str): + logger.debug(f"Loading localized tkeyword from DB lang:{lang}") + + query = ( + "select " + " tk.id," + " tk.about," + " tk.alt_label," + " tkl.label" + " from" + " base_thesaurus th," + " base_thesauruskeyword tk" + " left outer join " + " (select keyword_id, lang, label from base_thesauruskeywordlabel" + " where lang = %s) as tkl" + " on (tk.id = tkl.keyword_id)" + " where th.identifier = %s" + " and tk.thesaurus_id = th.id" + " order by label, alt_label" + ) + ret = [] + with connection.cursor() as cursor: + cursor.execute(query, [lang, thesaurus_identifier]) + for id, about, alt, label in cursor.fetchall(): + ret.append({"id": id, "about": about, "label": label or alt}) + return sorted(ret, key=lambda i: i["label"].lower()) + + +def get_localized_labels(lang, key="about"): + return {i[key]: i["label"] for i in get_localized_tkeywords(lang, I18N_THESAURUS_IDENTIFIER)} diff --git a/geonode/metadata/manager.py b/geonode/metadata/manager.py new file mode 100644 index 00000000000..5a33e23f90c --- /dev/null +++ b/geonode/metadata/manager.py @@ -0,0 +1,186 @@ +######################################################################### +# +# Copyright (C) 2024 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 +import copy +from cachetools import FIFOCache + +from django.utils.translation import gettext as _ + +from geonode.base.models import Thesaurus, ThesaurusKeyword, ThesaurusKeywordLabel +from geonode.metadata.handlers.abstract import MetadataHandler +from geonode.metadata.exceptions import UnsetFieldException +from geonode.metadata.i18n import get_localized_labels, I18N_THESAURUS_IDENTIFIER +from geonode.metadata.settings import MODEL_SCHEMA + +logger = logging.getLogger(__name__) + + +class MetadataManager: + """ + The metadata manager is the bridge between the API and the geonode model. + The metadata manager will loop over all of the registered metadata handlers, + calling their update_schema(jsonschema) which will add the subschemas of the + fields handled by each handler. At the end of the loop, the schema will be ready + to be delivered to the caller. + """ + + # FIFO bc we want to renew the data once in a while + _schema_cache = FIFOCache(32) + + def __init__(self): + self.root_schema = MODEL_SCHEMA + self.handlers = {} + + @classmethod + def clear_schema_cache(cls): + logger.info("Clearing schema cache") + while True: + try: + MetadataManager._schema_cache.popitem() + except KeyError: + return + + def add_handler(self, handler_id, handler): + self.handlers[handler_id] = handler() + + def _init_schema_context(self, lang): + # todo: cache localizations + return {"labels": get_localized_labels(lang)} + + def build_schema(self, lang=None): + logger.debug(f"build_schema {lang}") + + schema = copy.deepcopy(self.root_schema) + schema["title"] = _(schema["title"]) + + context = self._init_schema_context(lang) + + for key, handler in self.handlers.items(): + # logger.debug(f"build_schema: update schema -> {key}") + schema = handler.update_schema(schema, context, lang) + + # Set required fields. + required = [] + for fieldname, field in schema["properties"].items(): + if field.get("geonode:required", False): + required.append(fieldname) + + if required: + schema["required"] = required + return schema + + def get_schema(self, lang=None): + cache_key = str(lang) + ret = MetadataManager._schema_cache.get(cache_key, None) + if not ret: + logger.info(f"Building schema for {cache_key}") + ret = self.build_schema(lang) + MetadataManager._schema_cache[cache_key] = ret + logger.info("Schema built") + return ret + + def build_schema_instance(self, resource, lang=None): + schema = self.get_schema(lang) + + context = {} + for handler in self.handlers.values(): + handler.load_serialization_context(resource, schema, context) + + instance = {} + errors = {} + for fieldname, subschema in schema["properties"].items(): + # logger.debug(f"build_schema_instance: getting handler for property {fieldname}") + handler_id = subschema.get("geonode:handler", None) + if not handler_id: + logger.warning(f"Missing geonode:handler for schema property {fieldname}. Skipping") + continue + handler = self.handlers[handler_id] + try: + content = handler.get_jsonschema_instance(resource, fieldname, context, errors, lang) + instance[fieldname] = content + except UnsetFieldException: + pass + + # TESTING ONLY + if "error" in resource.title.lower(): + for fieldname in schema["properties"]: + MetadataHandler._set_error( + errors, [fieldname], f"TEST: test msg for field '{fieldname}' in GET request" + ) + instance["extraErrors"] = errors + + return instance + + def update_schema_instance(self, resource, json_instance) -> dict: + logger.debug(f"RECEIVED INSTANCE {json_instance}") + resource = resource.get_real_instance() + schema = self.get_schema() + context = {} + for handler in self.handlers.values(): + handler.load_deserialization_context(resource, schema, context) + + errors = {} + + for fieldname, subschema in schema["properties"].items(): + handler = self.handlers[subschema["geonode:handler"]] + try: + handler.update_resource(resource, fieldname, json_instance, context, errors) + except Exception as e: + MetadataHandler._set_error(errors, [fieldname], f"Error while processing this field: {e}") + try: + resource.save() + except Exception as e: + logger.warning(f"Error while updating schema instance: {e}") + MetadataHandler._set_error(errors, [], f"Error while saving the resource: {e}") + + # TESTING ONLY + if "error" in resource.title.lower(): + _create_test_errors(schema, errors, [], "TEST: field <{schema_type}>'{path}' PUT request") + + return errors + + +def _create_test_errors(schema, errors, path, msg_template, create_message=True): + if create_message: + stringpath = "/".join(path) if path else "ROOT" + MetadataHandler._set_error(errors, path, msg_template.format(path=stringpath, schema_type=schema["type"])) + + if schema["type"] == "object": + for field, subschema in schema["properties"].items(): + _create_test_errors(subschema, errors, path + [field], msg_template) + elif schema["type"] == "array": + _create_test_errors(schema["items"], errors, path, msg_template, create_message=False) + + +# signals for invalidating cached data +def thesaurus_changed(sender, instance, **kwargs): + base = f"Thesaurus changed: class {sender.__class__.__name__} -->" + if sender == Thesaurus and instance.identifier == I18N_THESAURUS_IDENTIFIER: + logger.debug(f"{base} {instance.identifier}") + MetadataManager.clear_schema_cache() + elif sender == ThesaurusKeyword and instance.thesaurus.identifier == I18N_THESAURUS_IDENTIFIER: + logger.debug(f"{base} {instance.about} ALT:{instance.alt_label}") + MetadataManager.clear_schema_cache() + elif sender == ThesaurusKeywordLabel and instance.keyword.thesaurus.identifier == I18N_THESAURUS_IDENTIFIER: + logger.debug(f"{base} {instance.keyword.about} ALT:{instance.keyword.alt_label} L:{instance.lang}") + MetadataManager.clear_schema_cache() + + +metadata_manager = MetadataManager() diff --git a/geonode/metadata/migrations/0001_initial.py b/geonode/metadata/migrations/0001_initial.py new file mode 100644 index 00000000000..498df8f6aab --- /dev/null +++ b/geonode/metadata/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.9 on 2024-11-25 10:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("base", "0092_migrate and_remove_resourcebase_files"), + ] + + operations = [ + migrations.CreateModel( + name="SparseField", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=64)), + ("value", models.CharField(blank=True, max_length=1024, null=True)), + ("resource", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="base.resourcebase")), + ], + options={ + "ordering": ("resource", "name"), + "unique_together": {("resource", "name")}, + }, + ), + ] diff --git a/geonode/metadata/migrations/__init__.py b/geonode/metadata/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/metadata/models.py b/geonode/metadata/models.py new file mode 100644 index 00000000000..111f76ffcb8 --- /dev/null +++ b/geonode/metadata/models.py @@ -0,0 +1,52 @@ +import logging + +from django.db import models + +from geonode.base.models import ResourceBase + + +logger = logging.getLogger(__name__) + +# class SparseFieldDecl(models.Model): +# class Type(enum.Enum): +# STRING = 'string' +# INTEGER = 'integer' +# FLOAT = 'float' +# BOOL = 'bool' +# +# FIELD_TYPES = [] +# name = models.CharField(max_length=64, null=False, blank=False, unique=True, primary_key=True) +# +# type = models.CharField(choices=[(x.value,x.name) for x in Type], max_length=32, null=False, blank=False, unique=True, ) +# nullable = models.BooleanField(default=True, null=False) +# +# eager = models.BooleanField(default=True, null=False) + + +class SparseField(models.Model): + """ + Sparse field related to a ResourceBase + """ + + resource = models.ForeignKey(ResourceBase, on_delete=models.CASCADE, null=False) + # name = models.ForeignKey(SparseFieldDecl, on_delete=models.PROTECT, null=False) + name = models.CharField(max_length=64, null=False, blank=False) + value = models.CharField(max_length=1024, null=True, blank=True) + + def __str__(self): + return f"{self.name}={self.value}" + + @staticmethod + def get_fields(resource: ResourceBase, names=None): + qs = SparseField.objects.filter(resource=resource) + if names: + qs = qs.filter(name__in=names) + + return qs.all() + + class Meta: + ordering = ( + "resource", + "name", + ) + unique_together = (("resource", "name"),) diff --git a/geonode/metadata/schemas/base.json b/geonode/metadata/schemas/base.json new file mode 100644 index 00000000000..7cc2b311313 --- /dev/null +++ b/geonode/metadata/schemas/base.json @@ -0,0 +1,159 @@ +{ + "uuid": { + "type": "string", + "title": "UUID", + "maxLength": 36, + "readOnly": true, + "NO_ui:widget": "hidden", + "geonode:handler": "base" + }, + "title": { + "type": "string", + "title": "title", + "description": "name by which the cited resource is known", + "maxLength": 255, + "geonode:handler": "base" + }, + "abstract": { + "type": "string", + "title": "abstract", + "description": "brief narrative summary of the content of the resource(s)", + "maxLength": 2000, + "ui:options": { + "widget": "textarea", + "rows": 5 + }, + "geonode:handler": "base" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "date_type": { + "type": "string", + "title": "date type", + "maxLength": 255 + }, + "category": { + "type": "object", + "title": "Category", + "description": "high-level geographic data thematic classification to assist in the grouping and search of available geographic data sets.", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": ["id"], + "geonode:required": true + }, + "language": { + "type": "string", + "title": "language", + "description": "language used within the dataset", + "maxLength": 16 + }, + "license": { + "type": "object", + "title": "License", + "description": "license of the dataset", + "maxLength": 255, + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": ["id"], + "geonode:required": true + }, + "attribution": { + "type": ["string", "null"], + "title": "Attribution", + "description": "authority or function assigned, as to a ruler, legislative assembly, delegate, or the like.", + "maxLength": 2048 + }, + "data_quality_statement": { + "type": ["string", "null"], + "title": "data quality statement", + "description": "general explanation of the data producer's knowledge about the lineage of a dataset", + "maxLength": 2000, + "ui:options": { + "widget": "textarea", + "rows": 5 + } + }, + "restriction_code_type": { + "type": "string", + "title": "restrictions", + "description": "limitation(s) placed upon the access or use of the data.", + "maxLength": 255 + }, + "constraints_other": { + "type": ["string", "null"], + "title": "Other constraints", + "description": "other restrictions and legal prerequisites for accessing and using the resource or metadata", + "ui:options": { + "widget": "textarea", + "rows": 5 + } + }, + "edition": { + "type": ["string", "null"], + "title": "edition", + "description": "version of the cited resource", + "maxLength": 255 + }, + "purpose": { + "type": ["string", "null"], + "title": "purpose", + "description": "summary of the intentions with which the resource(s) was developed", + "maxLength": 500, + "ui:options": { + "widget": "textarea", + "rows": 5 + }, + "geonode:handler": "base" + }, + "supplemental_information": { + "type": ["string", "null"], + "title": "supplemental information", + "description": "any other descriptive information about the dataset", + "maxLength": 2000, + "default": "No information provided", + "ui:options": { + "widget": "textarea", + "rows": 5 + } + }, + "temporal_extent_start": { + "type": ["string", "null"], + "format": "date-time", + "title": "temporal extent start", + "description": "time period covered by the content of the dataset (start)" + }, + "temporal_extent_end": { + "type": ["string", "null"], + "format": "date-time", + "title": "temporal extent end", + "description": "time period covered by the content of the dataset (end)" + }, + "maintenance_frequency": { + "type": "string", + "title": "maintenance frequency", + "description": "frequency with which modifications and deletions are made to the data after it is first produced", + "maxLength": 255 + }, + "spatial_representation_type": { + "type": "string", + "title": "spatial representation type", + "description": "method used to represent geographic information in the dataset.", + "maxLength": 255 + } + +} \ No newline at end of file diff --git a/geonode/metadata/settings.py b/geonode/metadata/settings.py new file mode 100644 index 00000000000..d2a7c786bfe --- /dev/null +++ b/geonode/metadata/settings.py @@ -0,0 +1,25 @@ +import os +from geonode.settings import PROJECT_ROOT + +MODEL_SCHEMA = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "{GEONODE_SITE}/resource.json", + "title": "GeoNode resource", + "type": "object", + "properties": {}, +} + +# The base schema is defined as a file in order to be customizable from other GeoNode instances +JSONSCHEMA_BASE = os.path.join(PROJECT_ROOT, "metadata/schemas/base.json") + +METADATA_HANDLERS = { + "base": "geonode.metadata.handlers.base.BaseHandler", + "thesaurus": "geonode.metadata.handlers.thesaurus.TKeywordsHandler", + "hkeyword": "geonode.metadata.handlers.hkeyword.HKeywordHandler", + "region": "geonode.metadata.handlers.region.RegionHandler", + "group": "geonode.metadata.handlers.group.GroupHandler", + "doi": "geonode.metadata.handlers.doi.DOIHandler", + "linkedresource": "geonode.metadata.handlers.linkedresource.LinkedResourceHandler", + "contact": "geonode.metadata.handlers.contact.ContactHandler", + "sparse": "geonode.metadata.handlers.sparse.SparseHandler", +} diff --git a/geonode/metadata/signals.py b/geonode/metadata/signals.py new file mode 100644 index 00000000000..ccb6e438a0b --- /dev/null +++ b/geonode/metadata/signals.py @@ -0,0 +1,17 @@ +import logging + +from django.db.models.signals import post_save + +from geonode.base.models import Thesaurus, ThesaurusKeyword, ThesaurusKeywordLabel +from geonode.metadata.manager import thesaurus_changed + + +logger = logging.getLogger(__name__) + + +def connect_signals(): + logger.debug("Setting up signal connections...") + post_save.connect(thesaurus_changed, sender=Thesaurus, weak=False, dispatch_uid="metadata_reset_t") + post_save.connect(thesaurus_changed, sender=ThesaurusKeyword, weak=False, dispatch_uid="metadata_reset_tk") + post_save.connect(thesaurus_changed, sender=ThesaurusKeywordLabel, weak=False, dispatch_uid="metadata_reset_tkl") + logger.debug("Signal connections set") diff --git a/geonode/metadata/tests/__init__.py b/geonode/metadata/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/metadata/tests/data/fake_base_schema.json b/geonode/metadata/tests/data/fake_base_schema.json new file mode 100644 index 00000000000..8ecff0a1d74 --- /dev/null +++ b/geonode/metadata/tests/data/fake_base_schema.json @@ -0,0 +1,32 @@ +{ + "uuid": { + "type": "string", + "title": "UUID", + "maxLength": 36, + "readOnly": true, + "NO_ui:widget": "hidden", + "geonode:handler": "base" + }, + "title": { + "type": "string", + "title": "title", + "description": "name by which the cited resource is known", + "maxLength": 255, + "geonode:handler": "base" + }, + "abstract": { + "type": "string", + "title": "abstract", + "description": "brief narrative summary of the content of the resource(s)", + "maxLength": 2000, + "ui:options": { + "widget": "textarea", + "rows": 5 + } + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + } +} \ No newline at end of file diff --git a/geonode/metadata/tests/data/fake_schema.json b/geonode/metadata/tests/data/fake_schema.json new file mode 100644 index 00000000000..d68cf662b95 --- /dev/null +++ b/geonode/metadata/tests/data/fake_schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "{GEONODE_SITE}/resource.json", + "title": "GeoNode resource", + "type": "object", + "properties": { + "field1": { + "type": "string", + "title": "fake_handler1", + "maxLength": 255, + "geonode:handler": "fake_handler1" + }, + "field2": { + "type": "string", + "title": "fake_handler2", + "maxLength": 255, + "geonode:handler": "fake_handler2" + }, + "field3": { + "type": "string", + "title": "fake_handler3", + "maxLength": 255, + "geonode:handler": "fake_handler3" + } + } + } \ No newline at end of file diff --git a/geonode/metadata/tests/test_handlers.py b/geonode/metadata/tests/test_handlers.py new file mode 100644 index 00000000000..a227a244622 --- /dev/null +++ b/geonode/metadata/tests/test_handlers.py @@ -0,0 +1,1837 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +import json +from unittest.mock import patch, MagicMock +from uuid import uuid4 +from datetime import datetime + +from django.contrib.auth import get_user_model +from django.test import RequestFactory +from django.utils.translation import gettext as _ +from django.contrib.auth.models import Group +from geonode.groups.models import GroupProfile +from geonode.people import Roles + +from geonode.metadata.settings import MODEL_SCHEMA +from geonode.base.models import ( + ResourceBase, + TopicCategory, + RestrictionCodeType, + License, + SpatialRepresentationType, + Region, + LinkedResource, + Thesaurus, + ThesaurusKeyword, + ThesaurusKeywordLabel, + ContactRole, +) +from geonode.settings import PROJECT_ROOT +from geonode.metadata.handlers.base import ( + BaseHandler, + CategorySubHandler, + DateTypeSubHandler, + DateSubHandler, + FrequencySubHandler, + LanguageSubHandler, + LicenseSubHandler, + RestrictionsSubHandler, + SpatialRepresentationTypeSubHandler, +) +from geonode.metadata.handlers.region import RegionHandler +from geonode.metadata.handlers.doi import DOIHandler +from geonode.metadata.handlers.linkedresource import LinkedResourceHandler +from geonode.metadata.handlers.group import GroupHandler +from geonode.metadata.handlers.hkeyword import HKeywordHandler +from geonode.metadata.handlers.thesaurus import TKeywordsHandler +from geonode.resource.utils import KeywordHandler +from geonode.metadata.handlers.contact import ContactHandler, ROLE_NAMES_MAP +from geonode.metadata.handlers.sparse import SparseHandler, sparse_field_registry +from geonode.metadata.models import SparseField +from geonode.metadata.exceptions import UnsetFieldException + +from geonode.tests.base import GeoNodeBaseTestSupport + + +class HandlersTests(GeoNodeBaseTestSupport): + + def setUp(self): + # set Json schemas + self.model_schema = MODEL_SCHEMA + self.lang = None + self.errors = {} + self.context = MagicMock() + + self.test_user = get_user_model().objects.create_user( + "user_1", "user_1@fakemail.com", "user_1_password", is_active=True + ) + + # Testing database setup + self.resource = ResourceBase.objects.create(title="Test Resource", uuid=str(uuid4()), owner=self.test_user) + self.extra_resource_1 = ResourceBase.objects.create( + title="Extra resource 1", uuid=str(uuid4()), owner=self.test_user + ) + self.extra_resource_2 = ResourceBase.objects.create( + title="Extra resource 2", uuid=str(uuid4()), owner=self.test_user + ) + self.extra_resource_3 = ResourceBase.objects.create( + title="Extra resource 3", uuid=str(uuid4()), owner=self.test_user + ) + + self.category = TopicCategory.objects.create( + identifier="fake_category", gn_description="a fake gn description", description="a detailed description" + ) + self.license = License.objects.create( + identifier="fake_license", name="a fake name", description="a detailed description" + ) + self.restrictions = RestrictionCodeType.objects.create( + identifier="fake_restrictions", description="a detailed description" + ) + self.spatial_repr = SpatialRepresentationType.objects.create( + identifier="fake_spatial_repr", description="a detailed description" + ) + self.fake_group = Group.objects.create(name="fake group") + # Create instances for thesaurus + self.thesaurus1 = Thesaurus.objects.create(title="Spatial scope thesaurus", identifier="3-2-4-3-spatialscope") + self.thesaurus2 = Thesaurus.objects.create( + title="INSPIRE themes thesaurus", identifier="3-2-4-1-gemet-inspire-themes" + ) + + # Create ThesaurusKeywords + self.keyword1 = ThesaurusKeyword.objects.create( + about="http://example.com/keyword1", + alt_label="Alt Label 1", + thesaurus=self.thesaurus1, + ) + self.keyword2 = ThesaurusKeyword.objects.create( + about="http://example.com/keyword2", + alt_label="Alt Label 2", + thesaurus=self.thesaurus2, + ) + + self.factory = RequestFactory() + + # Fake base schema path + self.fake_base_schema_path = os.path.join(PROJECT_ROOT, "metadata/tests/data/fake_base_schema.json") + + # Load fake base schema + with open(self.fake_base_schema_path) as f: + self.fake_base_schema = json.load(f) + + # Load a mocked schema + # Setup of the Manager + with open(os.path.join(PROJECT_ROOT, "metadata/tests/data/fake_schema.json")) as f: + self.fake_schema = json.load(f) + + # Handlers + self.base_handler = BaseHandler() + self.region_handler = RegionHandler() + self.linkedresource_handler = LinkedResourceHandler() + self.doi_handler = DOIHandler() + self.group_handler = GroupHandler() + self.hkeyword_handler = HKeywordHandler() + self.contact_handler = ContactHandler() + self.tkeywords_handler = TKeywordsHandler() + self.sparse_handler = SparseHandler() + + # A fake subschema + self.fake_subschema = { + "type": "string", + "title": "new field", + "description": "A new field was added", + "maxLength": 255, + } + + def tearDown(self): + super().tearDown() + + # Tests for the Base handler + @patch("geonode.metadata.handlers.base.SUBHANDLERS", new_callable=dict) + def test_base_handler_update_schema(self, mock_subhandlers): + """ + Ensure that the update_schema method gets a simple valid schema and + populate it with the base_schema properties accordignly + """ + + self.base_handler.json_base_schema = self.fake_base_schema_path + + # Model schema definition + jsonschema = self.model_schema + + # Mock subhandlers and update_subschema functionality, which + # will be used for the field "date" + field_name = "date" + mock_subhandlers[field_name] = MagicMock() + mock_subhandlers[field_name].update_subschema = MagicMock() + + def mock_update_subschema(subschema, lang=None): + subschema["oneOf"] = [{"const": "fake const", "title": "fake title"}] + + # Add the mock behavior for update_subschema + mock_subhandlers[field_name].update_subschema.side_effect = mock_update_subschema + + # Call the method + updated_schema = self.base_handler.update_schema(jsonschema, self.context, self.lang) + + # Check the full base schema + for field in self.fake_base_schema: + self.assertIn(field, updated_schema["properties"]) + + # Check subhandler execution for the field name "date" + self.assertEqual( + updated_schema["properties"]["date"].get("oneOf"), [{"const": "fake const", "title": "fake title"}] + ) + self.assertNotIn("oneOf", updated_schema["properties"]["uuid"]) + self.assertNotIn("oneOf", updated_schema["properties"]["title"]) + self.assertNotIn("oneOf", updated_schema["properties"]["abstract"]) + + # Check geonode:handler addition + self.assertEqual(updated_schema["properties"]["abstract"].get("geonode:handler"), "base") + self.assertEqual(updated_schema["properties"]["date"].get("geonode:handler"), "base") + + @patch("geonode.metadata.handlers.base.SUBHANDLERS", new_callable=dict) + def test_base_handler_get_jsonschema_instance_without_subhandlers(self, mock_subhandlers): + """ + Ensure that the get_json_schema_instance will get the db value + from a simple field + """ + + field_name = "title" + self.assertTrue(hasattr(self.resource, field_name), f"Field '{field_name}' does not exist.") + expected_field_value = self.resource.title + + # Call the method + field_value = self.base_handler.get_jsonschema_instance( + self.resource, field_name, self.context, self.errors, lang=None + ) + + # Ensure that the serialize method was not called + mock_subhandlers.get(field_name, MagicMock()).serialize.assert_not_called() + self.assertEqual(expected_field_value, field_value) + self.assertEqual(self.errors, {}) + + @patch("geonode.metadata.handlers.base.SUBHANDLERS", new_callable=dict) + def test_base_handler_get_jsonschema_instance_with_subhandlers(self, mock_subhandlers): + """ + Ensure that when a field name corresponds to a model in th ResourceBase, + the get_jsonschema_instance method gets a field_value which is a model + and assign it to the corresponding value. For testing we use the "category + field" + """ + + field_name = "category" + + # Create a fake resource + fake_resource = MagicMock() + + fake_resource.category = MagicMock() + fake_resource.category.identifier = "mocked_category_value" + expected_field_value = fake_resource.category.identifier + + # Add a SUBHANDLER for the field that returns the MagicMock model + mock_subhandlers[field_name] = MagicMock() + mock_subhandlers[field_name].serialize.return_value = expected_field_value + + # Call the method + field_value = self.base_handler.get_jsonschema_instance( + resource=fake_resource, field_name=field_name, context=self.context, errors=self.errors, lang=self.lang + ) + + # Ensure that the serialize method has been called once + mock_subhandlers[field_name].serialize.assert_called_once_with(fake_resource.category) + self.assertEqual(expected_field_value, field_value) + + @patch("geonode.metadata.handlers.base.SUBHANDLERS", new_callable=dict) + def test_update_resource_success_without_subhandlers(self, mock_subhandlers): + """ + Ensure that when a simple field name like title is set to the resource + without calling the SUBHANDLERS classes + """ + field_name = "title" + expected_field_value = "new_fake_title_value" + json_instance = {field_name: expected_field_value} + + # Call the method + self.base_handler.update_resource( + resource=self.resource, + field_name=field_name, + json_instance=json_instance, + context=self.context, + errors=self.errors, + ) + + # Ensure that the deserialize method was not called + mock_subhandlers.get(field_name, MagicMock()).deserialize.assert_not_called() + self.assertEqual(expected_field_value, self.resource.title) + + @patch("geonode.metadata.handlers.base.SUBHANDLERS", new_callable=dict) + def test_update_resource_success_with_subhandlers(self, mock_subhandlers): + """ + Ensure that when a field name corresponds to a model in th ResourceBase, + the update_resource method receives a field_value and assign it to the + corresponding model. For testing we use the "category field" + """ + field_name = "category" + field_value = "new_category_value" + json_instance = {field_name: field_value} + + # Fake resource object + fake_resource = MagicMock() + + # Simulate a MagicMock model for category + mock_category_model = MagicMock() + mock_category_model.identifier = field_value + + # Add a SUBHANDLER for the field that returns the MagicMock model + mock_subhandlers[field_name] = MagicMock() + mock_subhandlers[field_name].deserialize.return_value = mock_category_model + + # Call the method + self.base_handler.update_resource( + resource=fake_resource, + field_name=field_name, + json_instance=json_instance, + context=self.context, + errors=self.errors, + ) + + mock_subhandlers[field_name].deserialize.assert_called_once_with(field_value) + self.assertEqual(fake_resource.category, mock_category_model) + + @patch("geonode.metadata.handlers.base.SUBHANDLERS", new_callable=dict) + @patch("geonode.metadata.handlers.base.logger") + def test_update_resource_exception_handling(self, mock_logger, mock_subhandlers): + """ + Handling exception + """ + field_name = "category" + field_value = "new_category_value" + json_instance = {field_name: field_value} + + # Fake resource object + fake_resource = MagicMock() + + # Add a SUBHANDLER for the field that raises an exception during deserialization + mock_subhandlers[field_name] = MagicMock() + mock_subhandlers[field_name].deserialize.side_effect = Exception("Deserialization error") + + # Call the method + self.base_handler.update_resource( + resource=fake_resource, + field_name=field_name, + json_instance=json_instance, + context=self.context, + errors=self.errors, + ) + + mock_subhandlers[field_name].deserialize.assert_called_once_with(field_value) + + # Ensure that the exception is logged + mock_logger.warning.assert_called_once_with( + f"Error setting field {field_name}={field_value}: Deserialization error" + ) + + # Tests for subhandler classes of the base handler + @patch("geonode.metadata.handlers.base.reverse") + def test_category_subhandler_update_subschema(self, mocked_endpoint): + """ + Test for the update_subschema of the CategorySubHandler. + An instance of this model has been created initial setup + """ + + mocked_endpoint.return_value = "/mocked_url" + + subschema = {"type": "string", "title": "Category", "description": "a fake description", "maxLength": 255} + + # Call the update_subschema method with the real database data + CategorySubHandler.update_subschema(subschema, lang="en") + + # Assert changes to the subschema + self.assertIn("ui:options", subschema) + self.assertIn("geonode-ui:autocomplete", subschema["ui:options"]) + self.assertEqual(subschema["ui:options"]["geonode-ui:autocomplete"], mocked_endpoint.return_value) + + def test_category_subhandler_serialize_with_existed_db_value(self): + """ + Test the serialize method with existed db value. + An instance of this model has been created initial setup + """ + + # Test the case where the db_value is a model instance + serialized_value = CategorySubHandler.serialize(self.category) + + expected_value = {"id": self.category.identifier, "label": _(self.category.gn_description)} + + self.assertEqual(serialized_value, expected_value) + + # Assert that the serialize method returns the identifier + self.assertEqual(serialized_value["id"], self.category.identifier) + + def test_category_subhandler_serialize_invalid_data(self): + """ + Test the serialize method with invalid db value. + An instance of this model has been created initial setup + """ + + # Test the case where the db_value is not a model instance + non_category_value = "nonexistent value" + + invalid_serialized_value_1 = CategorySubHandler.serialize(non_category_value) + invalid_serialized_value_2 = CategorySubHandler.serialize(None) + + # Assert that the serialize method returns the input value unchanged + self.assertIsNone(invalid_serialized_value_1) + self.assertIsNone(invalid_serialized_value_2) + + def test_category_subhandler_deserialize(self): + """ + Test the deserialize method. + An instance of this model has been created initial setup + """ + + field_value = {"id": "fake_category"} + + # Call the method using the "fake_category" identifier from the created instance + deserialized_value = CategorySubHandler.deserialize(field_value) + + # Assert that the deserialized value is the correct model instance + self.assertEqual(deserialized_value, self.category) + self.assertEqual(deserialized_value.identifier, field_value["id"]) + + def test_category_subhandler_deserialize_with_invalid_data(self): + """ + Test the deserialize method with invalid data. + """ + + field_value = None + + # Call the method using the "fake_category" identifier from the created instance + deserialized_value = CategorySubHandler.deserialize(field_value) + + # Assert that the deserialized value is the correct model instance + self.assertIsNone(deserialized_value) + + @patch("geonode.metadata.handlers.base.reverse") + def test_license_subhandler_update_subschema(self, mocked_endpoint): + """ + Test for the update_subschema of the LicenseSubHandler. + An instance of this model has been created initial setup + """ + + mocked_endpoint.return_value = "/mocked_url" + + subschema = {"type": "string", "title": "License", "description": "a fake description", "maxLength": 255} + + # Call the update_subschema method with the real database data + LicenseSubHandler.update_subschema(subschema, lang="en") + + # Assert changes to the subschema + self.assertIn("ui:options", subschema) + self.assertIn("geonode-ui:autocomplete", subschema["ui:options"]) + self.assertEqual(subschema["ui:options"]["geonode-ui:autocomplete"], mocked_endpoint.return_value) + + def test_license_subhandler_serialize_with_existed_db_value(self): + """ + Test the serialize method with existed db value. + An instance of this model has been created initial setup + """ + + # Test the case where the db_value is a model instance + serialized_value = LicenseSubHandler.serialize(self.license) + + expected_value = {"id": self.license.identifier, "label": _(self.license.name)} + + self.assertEqual(serialized_value, expected_value) + + # Assert that the serialize method returns the identifier + self.assertEqual(serialized_value["id"], self.license.identifier) + + def test_license_subhandler_serialize_invalid_data(self): + """ + Test the serialize method with invalid db value. + An instance of this model has been created initial setup + """ + + # Test the case where the db_value is not a model instance + non_license_value = "nonexistent value" + + invalid_serialized_value_1 = LicenseSubHandler.serialize(non_license_value) + invalid_serialized_value_2 = LicenseSubHandler.serialize(None) + + # Assert that the serialize method returns the input value unchanged + self.assertIsNone(invalid_serialized_value_1) + self.assertIsNone(invalid_serialized_value_2) + + def test_license_subhandler_deserialize(self): + """ + Test the deserialize method. + An instance of this model has been created initial setup + """ + + field_value = {"id": "fake_license"} + + # Call the method using the "fake_category" identifier from the created instance + deserialized_value = LicenseSubHandler.deserialize(field_value) + + # Assert that the deserialized value is the correct model instance + self.assertEqual(deserialized_value, self.license) + self.assertEqual(deserialized_value.identifier, field_value["id"]) + + def test_license_subhandler_deserialize_with_invalid_data(self): + """ + Test the deserialize method with invalid data. + """ + + field_value = None + + # Call the method using the "fake_category" identifier from the created instance + deserialized_value = LicenseSubHandler.deserialize(field_value) + + # Assert that the deserialized value is the correct model instance + self.assertIsNone(deserialized_value) + + def test_restrictions_subhandler_update_subschema(self): + """ + Test for the update_subschema of the LicenseSubHandler. + An instance of the RestrictionCodeType model has been created + """ + + subschema = { + "type": "string", + "title": "restrictions", + "description": "limitation(s) placed upon the access or use of the data.", + "maxLength": 255, + } + + # Delete all the RestrictionCodeType models except the "fake_license" + fake_restrictions = RestrictionCodeType.objects.get(identifier="fake_restrictions") + RestrictionCodeType.objects.exclude(identifier=fake_restrictions.identifier).delete() + + # Call the update_subschema method with the real database data + RestrictionsSubHandler.update_subschema(subschema, lang="en") + + # Assertions + self.assertIn("oneOf", subschema) + self.assertEqual(len(subschema["oneOf"]), 1) + + # Check that each entry in "oneOf" contains the expected "const", "title", and "description" + self.assertEqual(subschema["oneOf"][0]["const"], "fake_restrictions") + self.assertEqual(subschema["oneOf"][0]["title"], "fake_restrictions") + self.assertEqual(subschema["oneOf"][0]["description"], "a detailed description") + + def test_restrictions_subhandler_serialize_with_existed_db_value(self): + """ + Test the serialize method with existed db value. + An instance of this model has been created initial setup + """ + + # Test the case where the db_value is a model instance + serialized_value = RestrictionsSubHandler.serialize(self.restrictions) + + # Assert that the serialize method returns the identifier + self.assertEqual(serialized_value, self.restrictions.identifier) + + def test_restrictions_subhandler_serialize_with_non_existed_db_value(self): + """ + Test the serialize method without an existed db value. + An instance of this model has been created initial setup + """ + + # Test the case where the db_value is not a model instance + non_restrictions_value = "nonexistent value" + + serialized_value = RestrictionsSubHandler.serialize(non_restrictions_value) + + # Assert that the serialize method returns the input value unchanged + self.assertEqual(serialized_value, non_restrictions_value) + + def test_restrictions_subhandler_deserialize(self): + """ + Test the deserialize method. + An instance of this model has been created initial setup + """ + + field_value = "fake_restrictions" + + # Call the method using the "fake_category" identifier from the created instance + deserialized_value = RestrictionsSubHandler.deserialize(field_value) + + # Assert that the deserialized value is the correct model instance + self.assertEqual(deserialized_value, self.restrictions) + self.assertEqual(deserialized_value.identifier, field_value) + + def test_restrictions_subhandler_deserialize_with_invalid_data(self): + """ + Test the deserialize method with invalid data. + """ + + field_value = None + + # Call the method using the "fake_category" identifier from the created instance + deserialized_value = RestrictionsSubHandler.deserialize(field_value) + + # Assert that the deserialized value is the correct model instance + self.assertIsNone(deserialized_value) + + def test_spatial_repr_type_subhandler_update_subschema(self): + """ + Test for the update_subschema of the SpatialRepresentationTypeSubHandler. + An instance of the SpatialRepresentationType model has been created + """ + + subschema = { + "type": "string", + "title": "spatial representation type", + "description": "method used to represent geographic information in the dataset.", + "maxLength": 255, + } + + # Delete all the SpatialRepresentationType models except the "fake_spatial_repr" + fake_spatial_repr = SpatialRepresentationType.objects.get(identifier="fake_spatial_repr") + SpatialRepresentationType.objects.exclude(identifier=fake_spatial_repr.identifier).delete() + + # Call the update_subschema method with the real database data + SpatialRepresentationTypeSubHandler.update_subschema(subschema, lang="en") + + # Assertions + self.assertIn("oneOf", subschema) + self.assertEqual(len(subschema["oneOf"]), 1) + + # Check that each entry in "oneOf" contains the expected "const", "title", and "description" + self.assertEqual(subschema["oneOf"][0]["const"], "fake_spatial_repr") + self.assertEqual(subschema["oneOf"][0]["title"], "fake_spatial_repr") + self.assertEqual(subschema["oneOf"][0]["description"], "a detailed description") + + def test_spatial_repr_type_subhandler_serialize_with_existed_db_value(self): + """ + Test the serialize method with existed db value. + An instance of this model has been created initial setup + """ + + # Test the case where the db_value is a model instance + serialized_value = SpatialRepresentationTypeSubHandler.serialize(self.spatial_repr) + + # Assert that the serialize method returns the identifier + self.assertEqual(serialized_value, self.spatial_repr.identifier) + + def test_spatial_repr_type_subhandler_serialize_with_non_existed_db_value(self): + """ + Test the serialize method without an existed db value. + An instance of this model has been created initial setup + """ + + # Test the case where the db_value is not a model instance + non_spatial_repr_value = "nonexistent value" + + serialized_value = SpatialRepresentationTypeSubHandler.serialize(non_spatial_repr_value) + + # Assert that the serialize method returns the input value unchanged + self.assertEqual(serialized_value, non_spatial_repr_value) + + def test_spatial_repr_type_subhandler_deserialize(self): + """ + Test the deserialize method. + An instance of this model has been created initial setup + """ + + field_value = "fake_spatial_repr" + + # Call the method using the "fake_category" identifier from the created instance + deserialized_value = SpatialRepresentationTypeSubHandler.deserialize(field_value) + + # Assert that the deserialized value is the correct model instance + self.assertEqual(deserialized_value, self.spatial_repr) + self.assertEqual(deserialized_value.identifier, field_value) + + def test_spatial_repr_type_subhandler_deserialize_with_invalid_data(self): + """ + Test the deserialize method with invalid data. + """ + + field_value = None + + # Call the method using the "fake_category" identifier from the created instance + deserialized_value = SpatialRepresentationTypeSubHandler.deserialize(field_value) + + # Assert that the deserialized value is the correct model instance + self.assertIsNone(deserialized_value) + + def test_date_type_subhandler_update_subschema(self): + """ + SubHandler test for the date type + """ + + # Prepare the initial subschema + subschema = {"type": "string", "title": "date type", "maxLength": 255} + + # Expected values for "oneOf" + expected_one_of_values = [ + {"const": "creation", "title": "Creation"}, + {"const": "publication", "title": "Publication"}, + {"const": "revision", "title": "Revision"}, + ] + + # Call the method to update the subschema + DateTypeSubHandler.update_subschema(subschema, lang="en") + + # Assertions + self.assertIn("oneOf", subschema) + self.assertEqual(subschema["oneOf"], expected_one_of_values) + self.assertIn("default", subschema) + self.assertEqual(subschema["default"], "Publication") + + def test_date_subhandler_serialize_with_valid_datetime(self): + """ + Subhandler test for the date serialization to the isoformat + """ + + test_datetime = datetime(2024, 12, 19, 15, 30, 45) + + # Call the serialize method + serialized_value = DateSubHandler.serialize(test_datetime) + + # Expected ISO 8601 format + expected_value = "2024-12-19T15:30:45" + + self.assertEqual(serialized_value, expected_value) + + def test_date_subhandler_serialize_without_datetime(self): + """ + Subhandler test for the date serialization to the isoformat with non + existent datetime object + """ + + test_value = "nonexistent datetime" + + # Call the serialize method + serialized_value = DateSubHandler.serialize(test_value) + + self.assertEqual(serialized_value, test_value) + + @patch( + "geonode.metadata.handlers.base.UPDATE_FREQUENCIES", + new=[ + ("fake_frequency1", _("Fake frequency 1")), + ("fake_frequency2", _("Fake frequency 2")), + ("fake_frequency3", _("Fake frequency 3")), + ], + ) + def test_frequency_subhandler_update_subschema(self): + """ + Subhandler test for the maintenance frequency + """ + + subschema = { + "type": "string", + "title": "maintenance frequency", + "description": "a detailed description", + "maxLength": 255, + } + + # Expected values for "oneOf" + expected_one_of_values = [ + {"const": "fake_frequency1", "title": "Fake frequency 1"}, + {"const": "fake_frequency2", "title": "Fake frequency 2"}, + {"const": "fake_frequency3", "title": "Fake frequency 3"}, + ] + + # Call the method to update the subschema + FrequencySubHandler.update_subschema(subschema, lang="en") + + self.assertIn("oneOf", subschema) + self.assertEqual(subschema["oneOf"], expected_one_of_values) + + @patch( + "geonode.metadata.handlers.base.ALL_LANGUAGES", + new=[ + ("fake_language1", "Fake language 1"), + ("fake_language2", "Fake language 2"), + ("fake_language3", "Fake language 3"), + ], + ) + def test_language_subhandler_update_subschema(self): + """ + Language subhandler test + """ + + subschema = { + "type": "string", + "title": "language", + "description": "language used within the dataset", + "maxLength": 255, + "default": "eng", + } + + # Expected values for "oneOf" + expected_one_of_values = [ + {"const": "fake_language1", "title": "Fake language 1"}, + {"const": "fake_language2", "title": "Fake language 2"}, + {"const": "fake_language3", "title": "Fake language 3"}, + ] + + # Call the method to update the subschema + LanguageSubHandler.update_subschema(subschema, lang="en") + + self.assertIn("oneOf", subschema) + self.assertEqual(subschema["oneOf"], expected_one_of_values) + + def test_add_sub_schema_without_after_what(self): + """ + This method is used by most of the handlers in the update_schema + method, in order to add the subschema to the desired place. + This test ensures the method's functionality without after_what + """ + + jsonschema = self.fake_schema + subschema = self.fake_subschema + property_name = "new_field" + + self.base_handler._add_subschema(jsonschema, property_name, subschema) + + self.assertIn(property_name, jsonschema["properties"]) + self.assertEqual(jsonschema["properties"][property_name], subschema) + + def test_add_sub_schema_with_after_what(self): + """ + This method is used by most of the handlers in the update_schema + method, in order to add the subschema to the desired place. + This test ensures the method's functionality with after_what + """ + + jsonschema = self.fake_schema + subschema = self.fake_subschema + property_name = "new_field" + + # Add the "new_field" after the field "field2" + self.base_handler._add_subschema(jsonschema, property_name, subschema, after_what="field2") + + self.assertIn(property_name, jsonschema["properties"]) + # Check that the new field has been added with the defined order + self.assertEqual(list(jsonschema["properties"].keys()), ["field1", "field2", "new_field", "field3"]) + + def test_add_subschema_with_nonexistent_after_what(self): + """ + This method is used by most of the handlers in the update_schema + method, in order to add the subschema to the desired place. + This test ensures the method's functionality with a non-existent + after_what + """ + + jsonschema = self.fake_schema + subschema = self.fake_subschema + property_name = "new_field" + + self.base_handler._add_subschema(jsonschema, property_name, subschema, after_what="nonexistent_property") + + # Check that the new property was added + self.assertIn(property_name, jsonschema["properties"]) + + # Check that the order is maintained as best as possible + self.assertEqual(list(jsonschema["properties"].keys()), ["field1", "field2", "field3", "new_field"]) + + # Check that the subschema was added + self.assertEqual(jsonschema["properties"][property_name], subschema) + + def test_add_subschema_to_empty_jsonschema(self): + """ + This method is used by most of the handlers in the update_schema + method, in order to add the subschema to the desired place. + This test ensures the method's functionality with an empty schema + """ + + jsonschema = {"properties": {}} + subschema = self.fake_subschema + property_name = "new_field" + + self.base_handler._add_subschema(jsonschema, property_name, subschema, after_what="nonexistent_property") + + self.assertIn(property_name, jsonschema["properties"]) + self.assertEqual(jsonschema["properties"][property_name], subschema) + + # Tests for the Region handler + + @patch("geonode.metadata.handlers.region.reverse") + def test_region_handler_update_schema(self, mock_reverse): + """ + Test for the update_schema of the region_handler + """ + + # fake schema definition which includes the "attribution" field + schema = { + "properties": { + "attribution": {"type": "string", "title": "attribution", "maxLength": 255}, + "fake_field": {"type": "string", "title": "fake_field", "maxLength": 255}, + } + } + mock_reverse.return_value = "/mocked_endpoint" + + # Define the expected regions schema + expected_regions = { + "type": "array", + "title": "Regions", + "description": "keyword identifies a location", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "label": {"type": "string", "title": "title"}, + }, + }, + "geonode:handler": "region", + "ui:options": {"geonode-ui:autocomplete": "/mocked_endpoint"}, + } + + # Call the method + updated_schema = self.region_handler.update_schema(schema, self.context, lang=self.lang) + + self.assertIn("regions", updated_schema["properties"]) + self.assertEqual(updated_schema["properties"]["regions"], expected_regions) + # Check that the new field has been added with the expected order + self.assertEqual(list(schema["properties"].keys()), ["attribution", "regions", "fake_field"]) + + def test_region_handler_get_jsonschema_instance(self): + """ + Test the get_jsonschema_instance of the region handler + using two region examples: Italy and Greece + """ + + # Add two Region instances to the ResourceBase instance + region_1 = Region.objects.get(code="ITA") + region_2 = Region.objects.get(code="GRC") + self.resource.regions.add(region_1, region_2) + + # Call the method to get the JSON schema instance + field_name = "regions" + + region_instance = self.region_handler.get_jsonschema_instance( + self.resource, field_name, self.context, self.errors, self.lang + ) + + # Assert that the JSON schema contains the regions we added + expected_region_instance = [ + {"id": str(region_1.id), "label": region_1.name}, + {"id": str(region_2.id), "label": region_2.name}, + ] + + self.assertEqual( + sorted(region_instance, key=lambda x: x["id"]), sorted(expected_region_instance, key=lambda x: x["id"]) + ) + + def test_region_handler_update_resource(self): + """ + Test the update resource of the region handler + using two region examples from the testing database + """ + + # Initially we add two Region instances to the ResourceBase instance + region_1 = Region.objects.get(code="GLO") + region_2 = Region.objects.get(code="AFR") + self.resource.regions.add(region_1, region_2) + + # Definition of the new regions which will be used from the tested method + # in order to update the database replacing the above regions with the regions below + updated_region_1 = Region.objects.get(code="ITA") + updated_region_2 = Region.objects.get(code="GRC") + region_3 = Region.objects.get(code="CYP") + + payload_data = { + "regions": [ + {"id": str(updated_region_1.id), "label": updated_region_1.name}, + {"id": str(updated_region_2.id), "label": updated_region_2.name}, + {"id": str(region_3.id), "label": region_3.name}, + ] + } + + # Call the method to get the JSON schema instance + field_name = "regions" + + # Call the method + self.region_handler.update_resource(self.resource, field_name, payload_data, self.context, self.errors) + + # Ensure that only the regions defined in the payload_data are included in the resource model + self.assertEqual( + sorted(self.resource.regions.all(), key=lambda region: region.name), + sorted([updated_region_1, updated_region_2, region_3], key=lambda region: region.name), + ) + + # Tests for the linkedresource handler + + @patch("geonode.metadata.handlers.linkedresource.reverse") + def test_linkedresource_handler_update_schema(self, mock_reverse): + """ + Test for the update_schema of the linkedresource + """ + + jsonschema = self.fake_schema + mock_reverse.return_value = "/mocked_endpoint" + + # Define the expected regions schema + expected_linked = { + "type": "array", + "title": _("Related resources"), + "description": _("Resources related to this one"), + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + }, + "label": {"type": "string", "title": _("title")}, + }, + }, + "geonode:handler": "linkedresource", + "ui:options": {"geonode-ui:autocomplete": "/mocked_endpoint"}, + } + + # Call the method + updated_schema = self.linkedresource_handler.update_schema(jsonschema, self.context, lang=self.lang) + + self.assertIn("linkedresources", updated_schema["properties"]) + self.assertEqual(updated_schema["properties"]["linkedresources"], expected_linked) + + def test_linkedresource_handler_get_jsonschema_instance(self): + """ + Test the get_jsonschema_instance of the linkedresource handler + """ + + # Add a linked resource to the main resource (self.resource) + linked_resource = LinkedResource.objects.create( + source=self.resource, + target=self.extra_resource_1, + ) + + field_name = "linkedresources" + + linkedresource_instance = self.linkedresource_handler.get_jsonschema_instance( + self.resource, field_name, self.context, self.errors, self.lang + ) + + expected_linkedresource_subschema = [ + {"id": str(linked_resource.target.id), "label": linked_resource.target.title}, + ] + + self.assertEqual(linkedresource_instance, expected_linkedresource_subschema) + + def test_linkedresource_handler_update_resource(self): + """ + Test the update resource of the linkedresource handler + """ + + # Add a linked resource just to test if it will be removed + # after the update_resource call + # Add a linked resource to the main resource (self.resource) + LinkedResource.objects.create( + source=self.resource, + target=self.extra_resource_3, + ) + + payload_data = {"linkedresources": [{"id": self.extra_resource_1.id}, {"id": self.extra_resource_2.id}]} + + # Call the method to get the JSON schema instance + field_name = "linkedresources" + + # Call the method + self.linkedresource_handler.update_resource(self.resource, field_name, payload_data, self.context, self.errors) + + # Verify the new links + linked_resources = LinkedResource.objects.filter(source=self.resource, internal=False) + linked_targets = [link.target for link in linked_resources] + self.assertIn(self.extra_resource_1, linked_targets) + self.assertIn(self.extra_resource_2, linked_targets) + # Ensure that the initial linked resource has been removed + self.assertNotIn(self.extra_resource_3, linked_targets) + + # Ensure that there is only one linked resource + self.assertEqual(len(linked_targets), 2) + + # Tests for the DOI handler + def test_doi_handler_update_schema(self): + """ + Test for the update_schema of the doi_handler + """ + + # fake_schema definition which includes the "edition" field + schema = { + "properties": { + "edition": {"type": "string", "title": "edition", "maxLength": 255}, + "fake_field": {"type": "string", "title": "fake_field", "maxLength": 255}, + } + } + + # Define the expected regions schema + expected_doi_subschema = { + "type": ["string", "null"], + "title": "DOI", + "description": _("a DOI will be added by Admin before publication."), + "maxLength": 255, + "geonode:handler": "doi", + } + + # Call the method + updated_schema = self.doi_handler.update_schema(schema, self.context, lang=self.lang) + + self.assertIn("doi", updated_schema["properties"]) + self.assertEqual(updated_schema["properties"]["doi"], expected_doi_subschema) + # Check that the new field has been added with the expected order + self.assertEqual(list(schema["properties"].keys()), ["edition", "doi", "fake_field"]) + + def test_doi_handler_get_jsonschema_instance(self): + """ + Test the get_jsonschema_instance of the doi handler + """ + + # Initially we add a doi to the ResourceBase instance + self.resource.doi = "10.1234/fake_doi" + + field_name = "doi" + + # Call the method + result = self.doi_handler.get_jsonschema_instance( + self.resource, field_name, self.context, self.errors, self.lang + ) + + # Assert the result is as expected + self.assertEqual(result, "10.1234/fake_doi") + + def test_doi_handler_update_resource(self): + """ + Test the update resource of the doi handler + """ + + # Initially we add a doi to the ResourceBase instance + self.resource.doi = "10.1234/fake_doi" + + payload_data = {"doi": "10.1000/new_fake_doi"} + + # Call the method to get the JSON schema instance + field_name = "doi" + + # Call the method + self.doi_handler.update_resource(self.resource, field_name, payload_data, self.context, self.errors) + + # Ensure that only the regions defined in the payload_data are included in the resource model + self.assertEqual(self.resource.doi, "10.1000/new_fake_doi") + + # Tests for the Group handler + @patch("geonode.metadata.handlers.group.reverse") + def test_group_handler_update_schema(self, mock_reverse): + """ + Test for the update_schema of the group_handler + """ + + mock_reverse.return_value = "/mocked_endpoint" + + # fake_schema definition which includes the "date_type" field + schema = { + "properties": { + "date_type": {"type": "string", "title": "date_type", "maxLength": 255}, + "fake_field": {"type": "string", "title": "fake_field", "maxLength": 255}, + } + } + + # Define the expected regions schema + expected_group_subschema = { + "type": "object", + "title": _("group"), + "properties": { + "id": { + "type": "string", + "ui:widget": "hidden", + }, + "label": { + "type": "string", + "title": _("group"), + }, + }, + "geonode:handler": "group", + "ui:options": {"geonode-ui:autocomplete": "/mocked_endpoint"}, + } + + # Call the method + updated_schema = self.group_handler.update_schema(schema, self.context, lang=self.lang) + + self.assertIn("group", updated_schema["properties"]) + self.assertEqual(updated_schema["properties"]["group"], expected_group_subschema) + # Check that the new field has been added with the expected order + self.assertEqual(list(schema["properties"].keys()), ["date_type", "group", "fake_field"]) + + def test_group_handler_get_jsonschema_instance_with_group(self): + """ + Test the get_jsonschema_instance of the group handler + """ + + GroupProfile.objects.create(group=self.fake_group, title="Test Group Profile") + self.resource.group = self.fake_group + + field_name = "group" + + expected_group_instance = { + "id": str(self.resource.group.groupprofile.pk), + "label": self.resource.group.groupprofile.title, + } + + group_instance = self.group_handler.get_jsonschema_instance( + self.resource, field_name, self.context, self.errors, self.lang + ) + + self.assertEqual(group_instance, expected_group_instance) + + def test_group_handler_get_jsonschema_instance_without_group(self): + """ + Test the get_jsonschema_instance of the group handler + in case that we don't have a group + """ + + field_name = "group" + + group_instance = self.group_handler.get_jsonschema_instance( + self.resource, field_name, self.context, self.errors, self.lang + ) + + self.assertIsNone(group_instance) + + def test_group_handler_update_resource_with_valid_id(self): + + group_profile = GroupProfile.objects.create(group=self.fake_group, title="Test Group Profile") + + field_name = "group" + payload_data = {"group": {"id": group_profile.pk}} + + # Call the method + self.group_handler.update_resource(self.resource, field_name, payload_data, self.context, self.errors) + + # Assert the resource group was updated + self.assertEqual(self.resource.group, group_profile.group) + + def test_group_handler_update_resource_with_no_id(self): + + field_name = "group" + json_instance = {"group": None} + + # Call the method + self.group_handler.update_resource(self.resource, field_name, json_instance, self.context, self.errors) + + # Assert the resource group was set to None + self.assertIsNone(self.resource.group) + + def test_group_handler_update_resource_without_field(self): + + field_name = "group" + json_instance = {} + + # Call the method + self.group_handler.update_resource(self.resource, field_name, json_instance, self.context, self.errors) + + # Assert the resource group was set to None + self.assertIsNone(self.resource.group) + + # Tests hkeyword handler + @patch("geonode.metadata.handlers.hkeyword.reverse") + def test_hkeyword_handler_update_schema(self, mock_reverse): + """ + Test for the update_schema of the hkeyword_handler + """ + + mock_reverse.return_value = "/mocked_endpoint" + + # fake_schema definition which includes the "tkeywords" field + schema = { + "properties": { + "tkeywords": {"type": "string", "title": "tkeywords", "maxLength": 255}, + "fake_field": {"type": "string", "title": "fake_field", "maxLength": 255}, + } + } + + # Define the expected regions schema + expected_hkeywords_subschema = { + "type": "array", + "title": _("Keywords"), + "description": _("Hierarchical keywords"), + "items": { + "type": "string", + }, + "ui:options": { + "geonode-ui:autocomplete": { + "url": "/mocked_endpoint", + "creatable": True, + }, + }, + "geonode:handler": "hkeyword", + } + + # Call the method + updated_schema = self.hkeyword_handler.update_schema(schema, self.context, lang=self.lang) + + self.assertIn("hkeywords", updated_schema["properties"]) + self.assertEqual(updated_schema["properties"]["hkeywords"], expected_hkeywords_subschema) + # Check that the new field has been added with the expected order + self.assertEqual(list(schema["properties"].keys()), ["tkeywords", "hkeywords", "fake_field"]) + + def test_hkeywords_handler_get_jsonschema_instance_with_keywords(self): + + field_name = "keywords" + + hkeywords = ["Keyword 1", "Keyword 2"] + KeywordHandler(self.resource, hkeywords).set_keywords() + + # Call the method + result = self.hkeyword_handler.get_jsonschema_instance(self.resource, field_name, self.context, self.errors) + + # Assert the correct list of keyword names with order-independent + expected = ["Keyword 1", "Keyword 2"] + self.assertCountEqual(result, expected) + + def test_hkeywords_handler_get_jsonschema_instance_no_keywords(self): + + # Ensure no keywords are defined with the resource + KeywordHandler(self.resource, []).set_keywords() + + field_name = "keywords" + + # Call the method + result = self.hkeyword_handler.get_jsonschema_instance(self.resource, field_name, self.context, self.errors) + + # Assert the result is an empty list + self.assertEqual(result, []) + + def test_hkeywords_handler_update_resource_with_valid_keywords(self): + + field_name = "hkeywords" + json_instance = {"hkeywords": ["new keyword 1", "new keyword 2"]} + + # Call the method + self.hkeyword_handler.update_resource(self.resource, field_name, json_instance, self.context, self.errors) + + # Assert that the keywords were correctly added to the resource + keywords = list(self.resource.keywords.all()) + keyword_names = [keyword.name for keyword in keywords] + expected_keywords = ["new keyword 1", "new keyword 2"] + self.assertCountEqual(keyword_names, expected_keywords) + + def test_hkeywords_handler_update_resource_without_keywords(self): + + field_name = "hkeywords" + json_instance = {"hkeywords": []} + + # Call the method + self.hkeyword_handler.update_resource(self.resource, field_name, json_instance, self.context, self.errors) + + self.assertEqual(self.resource.keywords.count(), 0) + + def test_hkeywords_handler_update_resource_with_null_empty_keywords(self): + + field_name = "hkeywords" + json_instance = {"hkeywords": [None, "valid keyword", ""]} + + # Call the method + self.hkeyword_handler.update_resource(self.resource, field_name, json_instance, self.context, self.errors) + + # Assert that the keywords were correctly added to the resource + keywords = list(self.resource.keywords.all()) + keyword_names = [keyword.name for keyword in keywords] + expected_keywords = ["valid keyword"] + self.assertCountEqual(keyword_names, expected_keywords) + + # Tests for contact handler + @patch("geonode.metadata.handlers.contact.reverse") + def test_contact_handler_update_schema(self, mock_reverse): + # Mock reverse function + mock_reverse.return_value = "/mocked/url" + + # Call update_schema + updated_schema = self.contact_handler.update_schema(self.fake_schema, self.context, self.lang) + + self.assertIn("contacts", updated_schema["properties"]) + + # Check if all roles are included in the contacts + contacts = updated_schema["properties"]["contacts"]["properties"] + for role in Roles: + rolename = ROLE_NAMES_MAP.get(role, role.name) + self.assertIn(rolename, contacts) + + contact = contacts[rolename] + self.assertIn("type", contact) + + if role.is_multivalue: + self.assertEqual(contact.get("type"), "array") + self.assertIn("minItems", contact) + self.assertIn("properties", contact["items"]) + if role.is_required: + self.assertEqual(contact["minItems"], 1) + else: + self.assertEqual(contact["minItems"], 0) + else: + self.assertEqual(contact.get("type"), "object") + self.assertIn("properties", contact) + # Assert 'id' field is required if the role is required + if role.is_required: + self.assertIn("id", contact["required"]) + else: + self.assertNotIn("id", contact["required"]) + + def test_contact_handler_get_jsonschema_instance(self): + + field_name = "contacts" + + # Create an author role for testing + author_role = get_user_model().objects.create_user( + "author_role", "author_role@fakemail.com", "new_fake_user_password", is_active=True + ) + + # Assign metadata author role + ContactRole.objects.create( + resource=self.resource, role=ROLE_NAMES_MAP[Roles.METADATA_AUTHOR], contact=author_role + ) + + # Call the method + result = self.contact_handler.get_jsonschema_instance( + self.resource, field_name, self.context, self.errors, self.lang + ) + + # Assert the output structure and content + self.assertIn(ROLE_NAMES_MAP[Roles.OWNER], result) + self.assertIn(ROLE_NAMES_MAP[Roles.METADATA_AUTHOR], result) + + # Check owner which is defined in the setUp method as test_user + owner_entry = result[ROLE_NAMES_MAP[Roles.OWNER]] + self.assertEqual(owner_entry["id"], str(self.test_user.id)) + self.assertEqual(owner_entry["label"], f"{self.test_user.username}") + + # Check metadata author + author_entry = result[ROLE_NAMES_MAP[Roles.METADATA_AUTHOR]] + self.assertEqual(len(author_entry), 1) # Assuming it's a multivalue role + self.assertEqual(author_entry[0]["id"], str(author_role.id)) + self.assertEqual(author_entry[0]["label"], f"{author_role.username}") + + def test_contact_handler_update_resource(self): + + field_name = "contacts" + + # Create a new owner instead of the initial test_user which is already defined as the owner + new_owner = get_user_model().objects.create_user( + "new_owner", "new_owner@fakemail.com", "new_owner_password", is_active=True + ) + + # Create an author role for testing + author_role = get_user_model().objects.create_user( + "author_role", "author_role@fakemail.com", "new_fake_user_password", is_active=True + ) + + # Prepare the JSON instance for updating + json_instance = { + field_name: { + ROLE_NAMES_MAP[Roles.OWNER]: {"id": str(new_owner.id), "label": f"{new_owner.username}"}, + ROLE_NAMES_MAP[Roles.METADATA_AUTHOR]: [ + {"id": str(author_role.id), "label": f"{author_role.username}"}, + ], + } + } + + # Call the method + self.contact_handler.update_resource(self.resource, field_name, json_instance, self.context, self.errors) + + # Assert the owner has been updated + self.assertEqual(self.resource.owner, new_owner) + + # Assert that the author role has been updated + contacts = self.resource.__get_contact_role_elements__(ROLE_NAMES_MAP[Roles.METADATA_AUTHOR]) + + self.assertEqual(len(contacts), 1) + self.assertEqual(contacts[0].id, author_role.id) + + # Tests for thesaurus handler + @patch("geonode.metadata.handlers.thesaurus.reverse") + @patch("geonode.metadata.handlers.thesaurus.TKeywordsHandler.collect_thesauri") + def test_tkeywords_handler_update_schema_with_thesauri(self, mock_collect_thesauri, mocked_endpoint): + + # fake_schema definition which includes the "category" field + schema = { + "properties": { + "category": {"type": "string", "title": "category", "maxLength": 255}, + "fake_field": {"type": "string", "title": "fake_field", "maxLength": 255}, + } + } + + # Mock data for collect_thesauri + mock_collect_thesauri.return_value = { + "3-2-4-3-spatialscope": { + "id": 1, + "card": {"minItems": 0, "maxItems": 1}, + "title": "Spatial scope", + "description": "Administrative level that the data set intends to cover.", + }, + "3-2-4-1-gemet-inspire-themes": { + "id": 2, + "card": {"minItems": 1}, + "title": "GEMET - INSPIRE themes, version 1.0", + "description": "GEMET - INSPIRE themes, version 1.0", + }, + } + + # Mock reverse to return a URL + mocked_endpoint.side_effect = lambda name, kwargs: f"/mocked/url/{kwargs['thesaurusid']}" + + # Call the method + updated_schema = self.tkeywords_handler.update_schema(schema, context={}, lang="en") + + # Assert tkeywords property is added + tkeywords = updated_schema["properties"].get("tkeywords") + self.assertIsNotNone(tkeywords) + self.assertEqual(tkeywords["type"], "object") + self.assertEqual(tkeywords["title"], "Keywords from Thesaurus") + + # Assert thesaurus structure for "3-2-4-3-spatialscope" + thesaurus = tkeywords["properties"]["3-2-4-3-spatialscope"] + self.assertEqual(thesaurus["type"], "array") + self.assertEqual(thesaurus["title"], "Spatial scope") + self.assertEqual( + thesaurus["description"], + "Administrative level that the data set intends to cover.", + ) + self.assertEqual(thesaurus["minItems"], 0) + self.assertEqual(thesaurus["maxItems"], 1) + self.assertEqual( + thesaurus["ui:options"]["geonode-ui:autocomplete"], + "/mocked/url/1", + ) + + # Assert thesaurus structure for "3-2-4-1-gemet-inspire-themes" + thesaurus = tkeywords["properties"]["3-2-4-1-gemet-inspire-themes"] + self.assertEqual(thesaurus["type"], "array") + self.assertEqual(thesaurus["title"], "GEMET - INSPIRE themes, version 1.0") + self.assertEqual( + thesaurus["description"], + "GEMET - INSPIRE themes, version 1.0", + ) + self.assertEqual(thesaurus["minItems"], 1) + self.assertNotIn("maxItems", thesaurus) + self.assertEqual( + thesaurus["ui:options"]["geonode-ui:autocomplete"], + "/mocked/url/2", + ) + + @patch("geonode.metadata.handlers.thesaurus.TKeywordsHandler.collect_thesauri") + def test_tkeywords_handler_update_schema_no_thesauri(self, mock_collect_thesauri): + + schema = { + "properties": { + "category": {"type": "string", "title": "category", "maxLength": 255}, + "fake_field": {"type": "string", "title": "fake_field", "maxLength": 255}, + } + } + + # Mock empty thesauri + mock_collect_thesauri.return_value = {} + + # Call the method + updated_schema = self.tkeywords_handler.update_schema(schema, context={}, lang="en") + + # Assert tkeywords property is hidden + tkeywords = updated_schema["properties"].get("tkeywords") + self.assertIsNotNone(tkeywords) + self.assertEqual(tkeywords["ui:widget"], "hidden") + + def test_tkeywords_handler_get_jsonschema_instance_translated_keywords(self): + """ + Test for the get_jsonschema_instance of the tkeywords handler. + In the setUp function we have set two keywords. In this test + we test the translated keywords + + """ + + # Create two instances of ThesaurusKeywordLabel + ThesaurusKeywordLabel.objects.create(keyword=self.keyword1, lang="en", label="Localized Label 1") + + ThesaurusKeywordLabel.objects.create(keyword=self.keyword2, lang="en", label="Localized Label 2") + + # Add the keywords to the resource: + self.resource.tkeywords.add(self.keyword1, self.keyword2) + + # Call the method + result = self.tkeywords_handler.get_jsonschema_instance( + resource=self.resource, field_name="tkeywords", context={}, errors=[], lang="en" + ) + + # Assertions for the results + # Check the structure for "3-2-4-3-spatialscope" + self.assertIn("3-2-4-3-spatialscope", result) + spatial_scope_keywords = result["3-2-4-3-spatialscope"] + self.assertEqual(len(spatial_scope_keywords), 1) + self.assertEqual(spatial_scope_keywords, [{"id": "http://example.com/keyword1", "label": "Localized Label 1"}]) + + # Check the structure for "3-2-4-1-gemet-inspire-themes" + self.assertIn("3-2-4-1-gemet-inspire-themes", result) + inspire_themes_keywords = result["3-2-4-1-gemet-inspire-themes"] + self.assertEqual(len(inspire_themes_keywords), 1) + self.assertEqual(inspire_themes_keywords, [{"id": "http://example.com/keyword2", "label": "Localized Label 2"}]) + + def test_tkeywords_handler_get_jsonschema_instance_untranslated_keywords(self): + """ + Test for the get_jsonschema_instance of the tkeywords handler. + In the setUp function we have set two keywords. In this test + we test the untranslated keywords + + """ + + # Add the keywords to the resource without using the ThesaurusKeywordLabel + self.resource.tkeywords.add(self.keyword1, self.keyword2) + + # Call the method + result = self.tkeywords_handler.get_jsonschema_instance( + resource=self.resource, field_name="tkeywords", context=self.context, errors=self.errors, lang=self.lang + ) + + # Assertions for the results + # Check the structure for "3-2-4-3-spatialscope" + self.assertIn("3-2-4-3-spatialscope", result) + spatial_scope_keywords = result["3-2-4-3-spatialscope"] + self.assertEqual(len(spatial_scope_keywords), 1) + self.assertEqual(spatial_scope_keywords, [{"id": "http://example.com/keyword1", "label": "Alt Label 1"}]) + + # Check the structure for "3-2-4-1-gemet-inspire-themes" + self.assertIn("3-2-4-1-gemet-inspire-themes", result) + inspire_themes_keywords = result["3-2-4-1-gemet-inspire-themes"] + self.assertEqual(len(inspire_themes_keywords), 1) + self.assertEqual(inspire_themes_keywords, [{"id": "http://example.com/keyword2", "label": "Alt Label 2"}]) + + def test_tkeywords_handler_update_resource(self): + """ + Ensures that the method will import the keyword1 and + the keyword2 in the database. It will not import the + keyword3 since it is not included in the ThesaurusKeyword + model + """ + + # JSON instance to simulate the input + json_instance = { + "tkeywords": { + "thes-1": [ + {"id": "http://example.com/keyword1", "label": "Keyword 1"}, + {"id": "http://example.com/keyword2", "label": "Keyword 2"}, + ], + "thes-2": [ + {"id": "http://example.com/keyword3", "label": "Keyword 3"}, + ], + } + } + + # Call the method + self.tkeywords_handler.update_resource( + resource=self.resource, + field_name="tkeywords", + json_instance=json_instance, + context=self.context, + errors=self.errors, + ) + + # Fetch the resource from the database and verify its tkeywords after running the update_resource + updated_keywords = self.resource.tkeywords.all() + + # Check the correct keywords are associated + expected_keywords = ThesaurusKeyword.objects.filter( + about__in=["http://example.com/keyword1", "http://example.com/keyword2"] + ) + + self.assertQuerysetEqual( + updated_keywords.order_by("id"), expected_keywords.order_by("id"), transform=lambda x: x + ) + + # Ensure that only the keyword1 and keyword2 are stored in the database + self.assertEqual(len(updated_keywords), 2) + + # Tests for the sparse handler + + def test_sparse_handler_update_schema(self): + + # Register two fields in the SparseFieldRegistry + sparse_field_registry.register( + field_name="new_sparse_field", schema={"type": "string", "title": "New Sparse Field"}, after="field1" + ) + + sparse_field_registry.register( + field_name="another_sparse_field", + schema={"type": "number", "title": "Another Sparse Field"}, + after="field2", + ) + + # Call the update_schema method using the fake_schema defined in the setUp method + updated_schema = self.sparse_handler.update_schema(self.fake_schema, self.context) + + # Assert that the new fields have been added + self.assertIn("new_sparse_field", updated_schema["properties"]) + self.assertIn("another_sparse_field", updated_schema["properties"]) + + # Check that the fields have the correct type + self.assertEqual(updated_schema["properties"]["new_sparse_field"]["type"], "string") + self.assertEqual(updated_schema["properties"]["another_sparse_field"]["type"], "number") + + # Check that the handler info was added + self.assertEqual(updated_schema["properties"]["new_sparse_field"]["geonode:handler"], "sparse") + self.assertEqual(updated_schema["properties"]["another_sparse_field"]["geonode:handler"], "sparse") + + # Check the order of the schema + self.assertEqual( + list(updated_schema["properties"].keys()), + ["field1", "new_sparse_field", "field2", "another_sparse_field", "field3"], + ) + + def test_sparse_handler_get_jsonschema_instance(self): + + CONTEXT_ID = "sparse" + + # Set up the context + self.context = { + CONTEXT_ID: { + "schema": { + "properties": { + "string_field": {"type": "string"}, + "number_field": {"type": "number"}, + "integer_field": {"type": "integer"}, + "nullable_field": {"type": ["null", "string"]}, + "array_field": {"type": "array"}, + "object_field": {"type": "object"}, + } + }, + "fields": { + "string_field": "test string", + "number_field": "42.0", + "integer_field": "7", + "nullable_field": None, + "array_field": '["item1", "item2"]', + "object_field": '{"key": "value"}', + }, + } + } + + # Test string field + result = self.sparse_handler.get_jsonschema_instance(self.resource, "string_field", self.context, self.errors) + self.assertEqual(result, "test string") + + # Test number field + result = self.sparse_handler.get_jsonschema_instance(self.resource, "number_field", self.context, self.errors) + self.assertEqual(result, 42.0) + + # Test integer field + result = self.sparse_handler.get_jsonschema_instance(self.resource, "integer_field", self.context, self.errors) + self.assertEqual(result, 7) + + # Test nullable field + result = self.sparse_handler.get_jsonschema_instance(self.resource, "nullable_field", self.context, self.errors) + self.assertIsNone(result) + + # Test array field + result = self.sparse_handler.get_jsonschema_instance(self.resource, "array_field", self.context, self.errors) + self.assertEqual(result, ["item1", "item2"]) + + # Test object field + result = self.sparse_handler.get_jsonschema_instance(self.resource, "object_field", self.context, self.errors) + self.assertEqual(result, {"key": "value"}) + + # Test missing field with no default and non-nullable + # with self.assertRaises(UnsetFieldException): + # self.sparse_handler.get_jsonschema_instance(self.resource, "missing_field", context, self.errors) + + # Test invalid number field + self.context[CONTEXT_ID]["fields"]["number_field"] = "invalid_number" + with self.assertRaises(UnsetFieldException): + self.sparse_handler.get_jsonschema_instance(self.resource, "number_field", self.context, self.errors) + + # Test invalid integer field + self.context[CONTEXT_ID]["fields"]["integer_field"] = "invalid_integer" + with self.assertRaises(UnsetFieldException): + self.sparse_handler.get_jsonschema_instance(self.resource, "integer_field", self.context, self.errors) + + # Test unhandled type + self.context[CONTEXT_ID]["schema"]["properties"]["unknown_field"] = {"type": "unknown"} + self.context[CONTEXT_ID]["fields"]["unknown_field"] = "some_value" + result = self.sparse_handler.get_jsonschema_instance(self.resource, "unknown_field", self.context, self.errors) + self.assertIsNone(result) + + def test_sparse_handler_update_resource(self): + + self.context = { + "sparse": { + "schema": { + "properties": { + "string_field": {"type": "string"}, + "number_field": {"type": "number"}, + "integer_field": {"type": "integer"}, + "nullable_field": {"type": ["null", "string"]}, + "array_field": {"type": "array"}, + "object_field": {"type": "object"}, + } + } + } + } + + # Test string field + json_instance_string_field = {"string_field": "updated string"} + self.sparse_handler.update_resource( + self.resource, "string_field", json_instance_string_field, self.context, self.errors + ) + sparse_field = SparseField.objects.get(resource=self.resource, name="string_field") + self.assertEqual(sparse_field.value, "updated string") + + # Test number field + json_instance_number_field = {"number_field": 123.45} + self.sparse_handler.update_resource( + self.resource, "number_field", json_instance_number_field, self.context, self.errors + ) + sparse_field = SparseField.objects.get(resource=self.resource, name="number_field") + self.assertEqual(float(sparse_field.value), 123.45) + + # Test nullable field + json_instance_nullable_field = {"nullable_field": None} + self.sparse_handler.update_resource( + self.resource, "nullable_field", json_instance_nullable_field, self.context, self.errors + ) + sparse_field = SparseField.objects.filter(resource=self.resource, name="nullable_field").first() + self.assertIsNone(sparse_field) + + # Test array field + json_instance_array_field = {"array_field": ["item1", "item2"]} + self.sparse_handler.update_resource( + self.resource, "array_field", json_instance_array_field, self.context, self.errors + ) + sparse_field = SparseField.objects.get(resource=self.resource, name="array_field") + self.assertEqual(sparse_field.value, '["item1", "item2"]') + + # Test object field + json_instance_object_field = {"object_field": {"key": "value"}} + self.sparse_handler.update_resource( + self.resource, "object_field", json_instance_object_field, self.context, self.errors + ) + sparse_field = SparseField.objects.get(resource=self.resource, name="object_field") + self.assertEqual(sparse_field.value, '{"key": "value"}') + + # Test invalid number + json_instance_invalid_number = {"number_field": "not_a_number"} + self.sparse_handler.update_resource( + self.resource, "number_field", json_instance_invalid_number, self.context, self.errors + ) + self.assertIn("Error parsing number", str(self.errors)) + + # Test invalid integer number + json_instance_invalid_int_number = {"integer_field": "not_an_integer"} + self.sparse_handler.update_resource( + self.resource, "integer_field", json_instance_invalid_int_number, self.context, self.errors + ) + self.assertIn("Error parsing integer", str(self.errors)) diff --git a/geonode/metadata/tests/tests.py b/geonode/metadata/tests/tests.py new file mode 100644 index 00000000000..9c862f493ec --- /dev/null +++ b/geonode/metadata/tests/tests.py @@ -0,0 +1,985 @@ +######################################################################### +# +# Copyright (C) 2024 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 os +import json +from unittest.mock import patch, MagicMock +from uuid import uuid4 + +from django.urls import reverse +from django.contrib.auth import get_user_model +from django.test import RequestFactory +from rest_framework import status +from django.utils.translation import gettext as _ + +from rest_framework.test import APITestCase +from geonode.metadata.settings import MODEL_SCHEMA +from geonode.metadata.manager import metadata_manager +from geonode.metadata.api.views import ( + ProfileAutocomplete, + MetadataLinkedResourcesAutocomplete, + MetadataRegionsAutocomplete, + MetadataHKeywordAutocomplete, + MetadataGroupAutocomplete, +) +from geonode.metadata.settings import METADATA_HANDLERS +from geonode.base.models import ResourceBase +from geonode.settings import PROJECT_ROOT +from geonode.base.models import ( + TopicCategory, + License, + Region, + HierarchicalKeyword, + ThesaurusKeyword, + ThesaurusKeywordLabel, + Thesaurus, +) +from geonode.groups.models import GroupProfile, GroupMember + + +class MetadataApiTests(APITestCase): + + def setUp(self): + # set Json schemas + self.model_schema = MODEL_SCHEMA + self.lang = None + + self.test_user_1 = get_user_model().objects.create_user( + "user_1", "user_1@fakemail.com", "user_1_password", is_active=True + ) + self.test_user_2 = get_user_model().objects.create_user( + "user_2", "user_2@fakemail.com", "user_2_password", is_active=True + ) + self.resource = ResourceBase.objects.create(title="Test Resource", uuid=str(uuid4()), owner=self.test_user_1) + self.other_resource = ResourceBase.objects.create( + title="Test other Resource", uuid=str(uuid4()), owner=self.test_user_1 + ) + self.factory = RequestFactory() + + # Setup of the Manager + with open(os.path.join(PROJECT_ROOT, "metadata/tests/data/fake_schema.json")) as f: + self.fake_schema = json.load(f) + + self.handler1 = MagicMock() + self.handler2 = MagicMock() + self.handler3 = MagicMock() + + self.fake_handlers = { + "fake_handler1": self.handler1, + "fake_handler2": self.handler2, + "fake_handler3": self.handler3, + } + + # Setup the database + TopicCategory.objects.create(identifier="cat1", gn_description="fake category 1") + TopicCategory.objects.create(identifier="cat2", gn_description="fake category 2") + License.objects.create(identifier="license1", name="fake license 1") + License.objects.create(identifier="license2", name="fake license 2") + Region.objects.create(code="fake_code_1", name="fake name 1") + Region.objects.create(code="fake_code_2", name="fake name 2") + HierarchicalKeyword.objects.create(name="fake_keyword_1", slug="fake keyword 1") + HierarchicalKeyword.objects.create(name="fake_keyword_2", slug="fake keyword 2") + + # Create groups for the metadata group autocomplete + self.group1 = GroupProfile.objects.create(title="Group A", slug="group_a") + self.group2 = GroupProfile.objects.create(title="Group B", slug="group_b") + + # Create Thesaurus keywords for the Thesaurus autocomplete + self.thesaurus = Thesaurus.objects.create(title="Spatial scope thesaurus", identifier="3-2-4-3-spatialscope") + self.thesaurus_id = self.thesaurus.id + # Create keywords for the thesaurus + self.keyword1 = ThesaurusKeyword.objects.create( + about="keyword1", alt_label="Alternative Label 1", thesaurus=self.thesaurus + ) + self.keyword2 = ThesaurusKeyword.objects.create( + about="keyword2", alt_label="Alternative Label 2", thesaurus=self.thesaurus + ) + self.keyword3 = ThesaurusKeyword.objects.create( + about="keyword3", alt_label="Alternative Label 3", thesaurus=self.thesaurus + ) + + ThesaurusKeywordLabel.objects.create(keyword=self.keyword1, label="Label 1", lang="en") + ThesaurusKeywordLabel.objects.create(keyword=self.keyword2, label="Label 2", lang="en") + ThesaurusKeywordLabel.objects.create(keyword=self.keyword3, label="Italiano etichetta", lang="it") + + def tearDown(self): + super().tearDown() + + # tests for the encpoint metadata/schema + def test_schema_valid_structure(self): + """ + Ensure the returned basic structure of the schema + """ + + url = reverse("metadata-schema") + + # Make a GET request to the action + response = self.client.get(url, format="json") + + # Assert that the response is in JSON format + self.assertEqual(response["Content-Type"], "application/json") + + # Check that the response status code is 200 + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Check the structure of the schema + response_data = response.json() + for field in self.model_schema.keys(): + self.assertIn(field, response_data) + + @patch("geonode.metadata.manager.metadata_manager.get_schema") + def test_schema_not_found(self, mock_get_schema): + """ + Test the behaviour of the schema endpoint + if the schema is not found + """ + mock_get_schema.return_value = None + + url = reverse("metadata-schema") + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {"Message": "Schema not found"}) + + @patch("geonode.metadata.manager.metadata_manager.get_schema") + def test_schema_with_lang(self, mock_get_schema): + """ + Test that the view recieves the lang parameter + """ + mock_get_schema.return_value = {"fake_schema": "schema"} + + url = reverse("metadata-schema") + response = self.client.get(url, {"lang": "it"}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {"fake_schema": "schema"}) + + # Verify that get_schema was called with the correct lang + mock_get_schema.assert_called_once_with("it") + + @patch("geonode.metadata.manager.metadata_manager.build_schema_instance") + @patch("geonode.base.api.permissions.UserHasPerms.has_permission", return_value=True) + def test_get_schema_instance_with_default_lang(self, mock_has_permission, mock_build_schema_instance): + """ + Test schema_instance endpoint with the default lang parameter + """ + + mock_build_schema_instance.return_value = {"fake_schema_instance": "schema_instance"} + + url = reverse("metadata-schema_instance", kwargs={"pk": self.resource.pk}) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertJSONEqual(response.content, {"fake_schema_instance": "schema_instance"}) + + # Ensure the mocked method was called + mock_build_schema_instance.assert_called() + + @patch("geonode.metadata.manager.metadata_manager.build_schema_instance") + @patch("geonode.base.api.permissions.UserHasPerms.has_permission", return_value=True) + def test_get_schema_instance_with_lang(self, mock_has_permission, mock_build_schema_instance): + """ + Test schema_instance endpoint with specific lang parameter + """ + + mock_build_schema_instance.return_value = {"fake_schema_instance": "schema_instance"} + + url = reverse("metadata-schema_instance", kwargs={"pk": self.resource.pk}) + response = self.client.get(url, {"lang": "it"}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertJSONEqual(response.content, {"fake_schema_instance": "schema_instance"}) + + # Ensure the mocked method was called with the correct arguments + mock_build_schema_instance.assert_called_once_with(self.resource, "it") + + @patch("geonode.metadata.manager.metadata_manager.update_schema_instance") + @patch("geonode.base.api.permissions.UserHasPerms.has_permission", return_value=True) + def test_put_patch_schema_instance_with_no_errors(self, mock_has_permission, mock_update_schema_instance): + """ + Test the success case of PATCH and PUT methods of the schema_instance + """ + + url = reverse("metadata-schema_instance", kwargs={"pk": self.resource.pk}) + fake_payload = {"field": "value"} + + # set the returned value of the mocked update_schema_instance with an empty dict + errors = {} + mock_update_schema_instance.return_value = errors + + response = self.client.put(url, data=fake_payload, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertJSONEqual( + response.content, {"message": "The resource was updated successfully", "extraErrors": errors} + ) + mock_update_schema_instance.assert_called_with(self.resource, fake_payload) + + @patch("geonode.metadata.manager.metadata_manager.update_schema_instance") + @patch("geonode.base.api.permissions.UserHasPerms.has_permission", return_value=True) + def test_put_patch_schema_instance_with_errors(self, mock_has_permission, mock_update_schema_instance): + """ + Test the PATCH and PUT methods of the schema_instance in case of errors + """ + + url = reverse("metadata-schema_instance", kwargs={"pk": self.resource.pk}) + fake_payload = {"field": "value"} + + # Set fake errors + errors = {"fake_error_1": "Field 'title' is required", "fake_error_2": "Invalid value for 'type'"} + mock_update_schema_instance.return_value = errors + + response = self.client.put(url, data=fake_payload, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertJSONEqual( + response.content, + {"message": "Some errors were found while updating the resource", "extraErrors": errors}, + ) + mock_update_schema_instance.assert_called_with(self.resource, fake_payload) + + @patch("geonode.base.api.permissions.UserHasPerms.has_permission", return_value=True) + def test_resource_not_found(self, mock_has_permission): + """ + Test case that the resource does not exist + """ + # Define a fake primary key + fake_pk = 1000 + + # Construct the URL for the action + url = reverse("metadata-schema_instance", kwargs={"pk": fake_pk}) + + # Perform a GET request + response = self.client.get(url) + + # Verify the response + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertJSONEqual(response.content, {"message": "The dataset was not found"}) + + # Tests for categories autocomplete + def test_categories_autocomplete_no_query(self): + """ + Test the response when no query parameter is provided + """ + + self.url = reverse("metadata_autocomplete_categories") + response = self.client.get(self.url) + + self.assertEqual(response.status_code, 200) + results = response.json()["results"] + self.assertEqual(len(results), 2) + self.assertIn({"id": "cat1", "label": _("fake category 1")}, results) + self.assertIn({"id": "cat2", "label": _("fake category 2")}, results) + + def test_categories_autocomplete_with_query(self): + """ + Test the response when a query parameter is provided + """ + + self.url = reverse("metadata_autocomplete_categories") + response = self.client.get(self.url, {"q": "fake cat"}) + + self.assertEqual(response.status_code, 200) + results = response.json()["results"] + self.assertEqual(len(results), 2) + self.assertIn({"id": "cat1", "label": _("fake category 1")}, results) + self.assertIn({"id": "cat2", "label": _("fake category 2")}, results) + + def test_categories_autocomplete_with_query_one_match(self): + """ + Test partial matches for the query parameter + """ + + self.url = reverse("metadata_autocomplete_categories") + response = self.client.get(self.url, {"q": "fake category 2"}) + + self.assertEqual(response.status_code, 200) + results = response.json()["results"] + self.assertEqual(len(results), 1) + self.assertIn({"id": "cat2", "label": _("fake category 2")}, results) + + def test_categories_autocomplete_with_query_no_match(self): + """ + Test when no results match the query parameter + """ + + self.url = reverse("metadata_autocomplete_categories") + response = self.client.get(self.url, {"q": "A missing category"}) + + self.assertEqual(response.status_code, 200) + results = response.json()["results"] + self.assertEqual(len(results), 0) + + # Tests for License autocomplete + def test_license_autocomplete_no_query(self): + """ + Test the response when no query parameter is provided + """ + + self.url = reverse("metadata_autocomplete_licenses") + response = self.client.get(self.url) + + self.assertEqual(response.status_code, 200) + results = response.json()["results"] + self.assertEqual(len(results), 2) + self.assertIn({"id": "license1", "label": _("fake license 1")}, results) + self.assertIn({"id": "license2", "label": _("fake license 2")}, results) + + def test_license_autocomplete_with_query(self): + """ + Test the response when a query parameter is provided + """ + + self.url = reverse("metadata_autocomplete_licenses") + response = self.client.get(self.url, {"q": "fake lic"}) + + self.assertEqual(response.status_code, 200) + results = response.json()["results"] + self.assertEqual(len(results), 2) + self.assertIn({"id": "license1", "label": _("fake license 1")}, results) + self.assertIn({"id": "license2", "label": _("fake license 2")}, results) + + def test_license_autocomplete_with_query_one_match(self): + """ + Test partial matches for the query parameter + """ + + self.url = reverse("metadata_autocomplete_licenses") + response = self.client.get(self.url, {"q": "fake license 2"}) + + self.assertEqual(response.status_code, 200) + results = response.json()["results"] + self.assertEqual(len(results), 1) + self.assertIn({"id": "license2", "label": _("fake license 2")}, results) + + def test_license_autocomplete_with_query_no_match(self): + """ + Test when no results match the query parameter + """ + + self.url = reverse("metadata_autocomplete_licenses") + response = self.client.get(self.url, {"q": "A missing category"}) + + self.assertEqual(response.status_code, 200) + results = response.json()["results"] + self.assertEqual(len(results), 0) + + # Tests for Profile autocomplete + + @patch("geonode.people.utils.get_available_users") + def test_profile_autocomplete_no_query(self, mock_get_available_users): + """ + Test that the queryset is restricted to available users + """ + + mocked_available_users = [self.test_user_1, self.test_user_2] + + mock_get_available_users.return_value = get_user_model().objects.filter( + pk__in=[u.pk for u in mocked_available_users] + ) + + request = self.factory.get(reverse("metadata_autocomplete_users")) + request.user = self.test_user_1 + + view = ProfileAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["label"], "user_1") + self.assertEqual(results[1]["label"], "user_2") + + @patch("geonode.people.utils.get_available_users") + def test_profile_autocomplete_with_query(self, mock_get_available_users): + """ + Test that the queryset is restricted to one user + """ + + mocked_available_users = [self.test_user_1, self.test_user_2] + + mock_get_available_users.return_value = get_user_model().objects.filter( + pk__in=[u.pk for u in mocked_available_users] + ) + + request = self.factory.get(reverse("metadata_autocomplete_users"), {"q": "user_2"}) + request.user = self.test_user_1 + + view = ProfileAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["label"], "user_2") + + @patch("geonode.people.utils.get_available_users") + def test_profile_autocomplete_no_match(self, mock_get_available_users): + """ + Test when there is no match of available users + """ + + mocked_available_users = [self.test_user_1, self.test_user_2] + + mock_get_available_users.return_value = get_user_model().objects.filter( + pk__in=[u.pk for u in mocked_available_users] + ) + + request = self.factory.get(reverse("metadata_autocomplete_users"), {"q": "missing_user"}) + request.user = self.test_user_1 + + view = ProfileAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + self.assertEqual(len(results), 0) + + # Tests for MetadataLinkedResourcesAutocomplete view + @patch("geonode.base.views.get_visible_resources") + def test_linked_resources_autocomplete_with_query(self, mock_get_visible_resources): + + request = self.factory.get("/metadata/autocomplete/resources", {"q": "Test other Resource"}) + request.user = self.test_user_1 + + # Mock the return value of get_visible_resources + mock_get_visible_resources.return_value = [self.other_resource] + + view = MetadataLinkedResourcesAutocomplete.as_view() + response = view(request) + + # Ensure the queryset was filtered by the query parameter + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["label"], "Test other Resource [resourcebase]") + + @patch("geonode.base.views.get_visible_resources") + def test_linked_resources_autocomplete_without_query(self, mock_get_visible_resources): + + request = self.factory.get("/metadata/autocomplete/resources") + request.user = self.test_user_1 + + # Mock the return value of get_visible_resources + mock_get_visible_resources.return_value = [self.resource, self.other_resource] + + view = MetadataLinkedResourcesAutocomplete.as_view() + response = view(request) + + # Ensure the queryset was filtered by the query parameter + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["label"], "Test Resource [resourcebase]") + self.assertEqual(results[1]["label"], "Test other Resource [resourcebase]") + + @patch("geonode.base.views.get_visible_resources") + def test_linked_resources_autocomplete_no_match(self, mock_get_visible_resources): + + request = self.factory.get("/metadata/autocomplete/resources") + request.user = self.test_user_1 + + # Mock the return value of get_visible_resources + mock_get_visible_resources.return_value = [] + + view = MetadataLinkedResourcesAutocomplete.as_view() + response = view(request) + + # Ensure the queryset was filtered by the query parameter + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + self.assertEqual(len(results), 0) + + # Tests for the Region autocomplete view + + def test_regions_autocomplete_without_query(self): + """ + Test filtering the queryset with the 'q' parameter + """ + # Simulate a request with a query parameter + request = self.factory.get("/metadata/autocomplete/regions") + + view = MetadataRegionsAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + + # Assert the results match the query + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["label"], "fake name 1") + self.assertEqual(results[1]["label"], "fake name 2") + + def test_regions_autocomplete_with_general_query(self): + """ + Test filtering the queryset with the 'q' parameter + """ + # Simulate a request with a query parameter + request = self.factory.get("/metadata/autocomplete/regions", {"q": "fake name"}) + + view = MetadataRegionsAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + + # Assert the results match the query + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["label"], "fake name 1") + self.assertEqual(results[1]["label"], "fake name 2") + + def test_regions_autocomplete_one_match(self): + """ + Test filtering the queryset with the 'q' parameter + """ + # Simulate a request with a query parameter + request = self.factory.get("/metadata/autocomplete/regions", {"q": "fake name 2"}) + + view = MetadataRegionsAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + + # Assert the results match the query + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["label"], "fake name 2") + + def test_regions_autocomplete_no_match(self): + """ + Test filtering the queryset with the 'q' parameter + """ + # Simulate a request with a query parameter + request = self.factory.get("/metadata/autocomplete/regions", {"q": "missing region"}) + + view = MetadataRegionsAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + results = json.loads(response.content)["results"] + + # Assert the results match the query + self.assertEqual(len(results), 0) + + # Tests for hkeyword_autocomplete view + def test_hkeyword_autocomplete_without_query(self): + + request = self.factory.get("/metadata/autocomplete/hkeywords") + + view = MetadataHKeywordAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + # Assert all keywords are returned + self.assertEqual(len(results), 2) + self.assertIn("fake_keyword_1", results) + self.assertIn("fake_keyword_2", results) + + def test_hkeyword_autocomplete_with_query(self): + + request = self.factory.get("/metadata/autocomplete/hkeywords", {"q": "fake keyword"}) + + view = MetadataHKeywordAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + # Assert all keywords are returned + self.assertEqual(len(results), 2) + self.assertIn("fake_keyword_1", results) + self.assertIn("fake_keyword_2", results) + + def test_hkeyword_autocomplete_one_match(self): + + request = self.factory.get("/metadata/autocomplete/hkeywords", {"q": "fake keyword 2"}) + + view = MetadataHKeywordAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + # Assert all keywords are returned + self.assertEqual(len(results), 1) + self.assertIn("fake_keyword_2", results) + + def test_hkeyword_autocomplete_no_match(self): + + request = self.factory.get("/metadata/autocomplete/hkeywords", {"q": "missing keyword"}) + + view = MetadataHKeywordAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + # Assert all keywords are returned + self.assertEqual(len(results), 0) + + # Tests for the Metadata GroupProfile autocomplete + def test_metadata_group_autocomplete_no_user(self): + """ + Test group autocomplete when the user is None + """ + request = self.factory.get("/metadata/autocomplete/groups") + request.user = None + view = MetadataGroupAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + self.assertEqual(len(results), 0) + self.assertEqual(results, []) + + def test_metadata_group_autocomplete_superuser(self): + """ + Test group autocomplete when the user is SuperUser + """ + + # Create a superuser + superuser = get_user_model().objects.create_superuser( + username="superuser", email="superuser@example.com", password="password" + ) + + request = self.factory.get("/metadata/autocomplete/groups") + request.user = superuser + view = MetadataGroupAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + # Assert the results match the query + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["label"], "Group A") + self.assertEqual(results[1]["label"], "Group B") + + def test_metadata_group_autocomplete_staff_user(self): + """ + Test group autocomplete when the user is staff user + """ + + # Create a staff user + staff_user = get_user_model().objects.create_user( + username="staff", email="staff@example.com", password="password", is_staff=True + ) + + request = self.factory.get("/metadata/autocomplete/groups") + request.user = staff_user + view = MetadataGroupAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + # Assert the results match the query + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["label"], "Group A") + self.assertEqual(results[1]["label"], "Group B") + + def test_metadata_group_autocomplete_simple_user(self): + """ + Test group autocomplete when the user is a simple user + with an associated group (Group B) + """ + + # Associate Group B with test_user_1 + GroupMember.objects.create(user=self.test_user_1, group=self.group2) + + request = self.factory.get("/metadata/autocomplete/groups") + request.user = self.test_user_1 + view = MetadataGroupAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + # Assert the results match the query + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["label"], "Group B") + + def test_metadata_group_autocomplete_superuser_with_query(self): + """ + Test group autocomplete when the user is SuperUser + and creates a specific query + """ + superuser = get_user_model().objects.create_superuser( + username="superuser", email="superuser@example.com", password="password" + ) + + request = self.factory.get("/metadata/autocomplete/groups", {"q": "Group A"}) + request.user = superuser + view = MetadataGroupAutocomplete.as_view() + response = view(request) + + self.assertEqual(response.status_code, 200) + + results = json.loads(response.content)["results"] + + # Assert the results match the query + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["label"], "Group A") + + # Tests for Thesaurus autocomplete + + def test_tkeywords_autocomplete_no_query(self): + + url = reverse("metadata_autocomplete_tkeywords", kwargs={"thesaurusid": self.thesaurus_id}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + results = response.json()["results"] + self.assertEqual(len(results), 3) + + self.assertIn({"id": "keyword1", "label": "Label 1"}, results) + self.assertIn({"id": "keyword2", "label": "Label 2"}, results) + + # Check that untranslated keywords are prefixed with "!" + self.assertIn({"id": "keyword3", "label": "! Alternative Label 3"}, results) + + def test_tkeywords_autocomplete_with_query(self): + """ + Test of the tkeywords autocomplete view using a specific query. + Keep in mind that the non-translated keywords will be included + in the result + """ + + url = reverse("metadata_autocomplete_tkeywords", kwargs={"thesaurusid": self.thesaurus_id}) + response = self.client.get(url, {"q": "Label 1"}) + self.assertEqual(response.status_code, 200) + + results = response.json()["results"] + + # Ensure that the Label 2 is not included in the result + self.assertEqual(len(results), 2) + self.assertIn({"id": "keyword1", "label": "Label 1"}, results) + self.assertIn({"id": "keyword3", "label": "! Alternative Label 3"}, results) + + # Manager tests + + def test_registry_and_add_handler(self): + + self.assertEqual(set(metadata_manager.handlers.keys()), set(METADATA_HANDLERS.keys())) + for handler_id in METADATA_HANDLERS.keys(): + self.assertIn(handler_id, metadata_manager.handlers) + + @patch( + "geonode.metadata.manager.metadata_manager.root_schema", + new_callable=lambda: { + "title": "Test Schema", + "properties": { + "field1": {"type": "string"}, + "field2": {"type": "integer", "geonode:required": True}, + }, + }, + ) + @patch("geonode.metadata.manager.metadata_manager._init_schema_context") + def test_build_schema(self, mock_init_schema_context, mock_root_schema): + + mock_init_schema_context.return_value = {} + + with patch.dict(metadata_manager.handlers, self.fake_handlers, clear=True): + + self.handler1.update_schema.side_effect = lambda schema, context, lang: schema + self.handler2.update_schema.side_effect = lambda schema, context, lang: schema + self.handler3.update_schema.side_effect = lambda schema, context, lang: schema + + # Call build_schema + schema = metadata_manager.build_schema(lang="en") + + self.assertEqual(schema["title"], "Test Schema") + self.assertIn("field1", schema["properties"]) + self.assertIn("field2", schema["properties"]) + self.assertIn("required", schema) + self.assertIn("field2", schema["required"]) + self.handler1.update_schema.assert_called() + self.handler2.update_schema.assert_called() + self.handler3.update_schema.assert_called() + + @patch("geonode.metadata.manager.metadata_manager.build_schema") + @patch("cachetools.FIFOCache.get") + # Mock FIFOCache's __setitem__ method (cache setting) + @patch("cachetools.FIFOCache.__setitem__") + def test_get_schema(self, mock_setitem, mock_get, mock_build_schema): + + lang = "en" + expected_schema = self.fake_schema + + # Case when the schema is already in cache + mock_get.return_value = expected_schema + result = metadata_manager.get_schema(lang) + + # Assert that the schema was retrieved from the cache + mock_get.assert_called_once_with(str(lang), None) + mock_build_schema.assert_not_called() + self.assertEqual(result, expected_schema) + + # Reset mock calls to test the second case + mock_get.reset_mock() + mock_build_schema.reset_mock() + mock_setitem.reset_mock() + + # Case when the schema is not in cache + mock_get.return_value = None + mock_build_schema.return_value = expected_schema + result = metadata_manager.get_schema(lang) + + mock_get.assert_called_once_with(str(lang), None) + mock_build_schema.assert_called_once_with(lang) + mock_setitem.assert_called_once_with(str(lang), expected_schema) + self.assertEqual(result, expected_schema) + + @patch("geonode.metadata.manager.metadata_manager.get_schema") + def test_build_schema_instance_no_errors(self, mock_get_schema): + + self.lang = "en" + mock_get_schema.return_value = self.fake_schema + + with patch.dict(metadata_manager.handlers, self.fake_handlers, clear=True): + + self.handler1.get_jsonschema_instance.return_value = {"data from fake handler 1"} + self.handler2.get_jsonschema_instance.return_value = {"data from fake handler 2"} + self.handler3.get_jsonschema_instance.return_value = {"data from fake handler 3"} + + # Call the method + instance = metadata_manager.build_schema_instance(self.resource, self.lang) + + # Assert that the handlers were called and instance was built correctly + self.handler1.get_jsonschema_instance.assert_called_once_with(self.resource, "field1", {}, {}, self.lang) + self.handler2.get_jsonschema_instance.assert_called_once_with(self.resource, "field2", {}, {}, self.lang) + self.handler3.get_jsonschema_instance.assert_called_once_with(self.resource, "field3", {}, {}, self.lang) + + self.assertEqual(instance["field1"], {"data from fake handler 1"}) + self.assertEqual(instance["field2"], {"data from fake handler 2"}) + self.assertEqual(instance["field3"], {"data from fake handler 3"}) + self.assertNotIn("extraErrors", instance) + + @patch("geonode.metadata.manager.metadata_manager.get_schema") + def test_update_schema_instance_no_errors(self, mock_get_schema): + + # json_instance is the payload from the client. + # In this test is used only to call the update_schema_instance + json_instance = {"field1": "new_value1", "new_field2": "new_value2"} + + mock_get_schema.return_value = self.fake_schema + # Mock the save method + self.resource.save = MagicMock() + + with patch.dict(metadata_manager.handlers, self.fake_handlers, clear=True): + + # Simulate successful handler behavior + self.handler1.update_resource.return_value = None + self.handler2.update_resource.return_value = None + self.handler3.update_resource.return_value = None + + # Call the update_schema_instance method + errors = metadata_manager.update_schema_instance(self.resource, json_instance) + + # Assert that handlers were called to update the resource with the correct data + self.handler1.update_resource.assert_called_once_with(self.resource, "field1", json_instance, {}, {}) + self.handler2.update_resource.assert_called_once_with(self.resource, "field2", json_instance, {}, {}) + self.handler3.update_resource.assert_called_once_with(self.resource, "field3", json_instance, {}, {}) + + # Assert no errors were raised + self.assertEqual(errors, {}) + + # Check that resource.save() is called + self.resource.save.assert_called_once() + + # Assert that there were no extra errors in the response + self.assertNotIn("extraErrors", errors) + + @patch("geonode.metadata.manager.metadata_manager.get_schema") + def test_update_schema_instance_with_handler_error(self, mock_get_schema): + + # json_instance is the payload from the client. + # In this test is used only to call the update_schema_instance + json_instance = {} + + mock_get_schema.return_value = self.fake_schema + + # Mock the save method + self.resource.save = MagicMock() + + with patch.dict(metadata_manager.handlers, self.fake_handlers, clear=True): + + # Simulate an error in update_resource for handler2 + self.handler1.update_resource.side_effect = None + self.handler2.update_resource.side_effect = Exception("Error in handler2") + self.handler3.update_resource.side_effect = None + + # Call the method under test + errors = metadata_manager.update_schema_instance(self.resource, json_instance) + + # Assert that update_resource was called for each handler + self.handler1.update_resource.assert_called() + self.handler2.update_resource.assert_called() + self.handler3.update_resource.assert_called() + + # Assert that resource.save() was called + self.resource.save.assert_called_once() + + # Verify that errors are collected for handler2 + self.assertIn("field2", errors) + self.assertEqual(errors["field2"]["__errors"], ["Error while processing this field: Error in handler2"]) + + # Verify that no other errors were added for handler1 and handler3 + self.assertNotIn("field1", errors) + self.assertNotIn("field3", errors) + + @patch("geonode.metadata.manager.metadata_manager.get_schema") + def test_update_schema_instance_with_db_error(self, mock_get_schema): + + # json_instance is the payload from the client. + # In this test is used only to call the update_schema_instance + json_instance = {} + + mock_get_schema.return_value = self.fake_schema + + # Mock save method with an exception + self.resource.save = MagicMock(side_effect=Exception("Error during the resource save")) + + with patch.dict(metadata_manager.handlers, self.fake_handlers, clear=True): + + self.handler1.update_resource.side_effect = None + self.handler2.update_resource.side_effect = None + self.handler3.update_resource.side_effect = None + + # Call the method under test + errors = metadata_manager.update_schema_instance(self.resource, json_instance) + + # Assert that update_resource was called for each handler + self.handler1.update_resource.assert_called() + self.handler2.update_resource.assert_called() + self.handler3.update_resource.assert_called() + + # Assert that save raised an error and was recorded + self.resource.save.assert_called_once() + self.assertIn("__errors", errors) + self.assertEqual(errors["__errors"], ["Error while saving the resource: Error during the resource save"]) diff --git a/geonode/metadata/urls.py b/geonode/metadata/urls.py new file mode 100644 index 00000000000..7e96ae33c7b --- /dev/null +++ b/geonode/metadata/urls.py @@ -0,0 +1,3 @@ +from geonode.metadata.api.urls import urlpatterns + +urlpatterns += [] # make flake8 happy diff --git a/geonode/metadata/views.py b/geonode/metadata/views.py new file mode 100644 index 00000000000..f8417b96341 --- /dev/null +++ b/geonode/metadata/views.py @@ -0,0 +1 @@ +# Create your views here diff --git a/geonode/people/__init__.py b/geonode/people/__init__.py index 5af39af536c..37e0c8a1c30 100644 --- a/geonode/people/__init__.py +++ b/geonode/people/__init__.py @@ -79,3 +79,7 @@ def get_multivalue_ones(cls): @classmethod def get_toggled_ones(cls): return [role for role in cls if role.is_toggled_in_metadata_editor] + + @classmethod + def get_role_by_name(cls, name): + return next((role for role in cls if role.name == name)) diff --git a/geonode/settings.py b/geonode/settings.py index fc854a583b4..17532826c7d 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -408,6 +408,7 @@ "geonode.catalogue", "geonode.catalogue.metadataxsl", "geonode.harvesting", + "geonode.metadata", ) # GeoNode Apps diff --git a/geonode/urls.py b/geonode/urls.py index f65f402f461..87d489d4edd 100644 --- a/geonode/urls.py +++ b/geonode/urls.py @@ -135,6 +135,8 @@ re_path(r"^api/v2/api-auth/", include("rest_framework.urls", namespace="geonode_rest_framework")), re_path(r"^api/v2/", include("geonode.facets.urls")), re_path(r"^api/v2/", include("geonode.assets.urls")), + # metadata views + re_path(r"^api/v2/", include("geonode.metadata.urls")), re_path(r"", include(api.urls)), re_path( r"uploads/upload", From 1c40f06329d6f24783f29f7a190bb2923d302e82 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:24:32 +0100 Subject: [PATCH 34/61] [Fixes #12789] Improve 3dtiles filename handling (#12826) * [Fixes #12789] Improve 3dtiles filename handling * [Fixes #12789] Improve 3dtiles filename handling --- geonode/upload/handlers/tiles3d/handler.py | 31 +++++++++++++--------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/geonode/upload/handlers/tiles3d/handler.py b/geonode/upload/handlers/tiles3d/handler.py index 434b567f018..c80511b38b1 100755 --- a/geonode/upload/handlers/tiles3d/handler.py +++ b/geonode/upload/handlers/tiles3d/handler.py @@ -82,8 +82,7 @@ def can_handle(_data) -> bool: if not base: return False ext = base.split(".")[-1] if isinstance(base, str) else base.name.split(".")[-1] - input_filename = os.path.basename(base if isinstance(base, str) else base.name) - if ext in ["json"] and "tileset.json" in input_filename: + if ext in ["json"] and Tiles3DFileHandler.is_3dtiles_json(base): return True return False @@ -110,16 +109,7 @@ def is_valid(files, user, **kwargs): raise Invalid3DTilesException("Please remove the additional dots in the filename") try: - with open(_file, "r") as _readed_file: - _file = json.loads(_readed_file.read()) - # required key described in the specification of 3dtiles - # https://docs.ogc.org/cs/22-025r4/22-025r4.html#toc92 - is_valid = all(key in _file.keys() for key in ("asset", "geometricError", "root")) - - if not is_valid: - raise Invalid3DTilesException( - "The provided 3DTiles is not valid, some of the mandatory keys are missing. Mandatory keys are: 'asset', 'geometricError', 'root'" - ) + _file = Tiles3DFileHandler.is_3dtiles_json(_file) Tiles3DFileHandler.validate_3dtile_payload(payload=_file) @@ -128,6 +118,21 @@ def is_valid(files, user, **kwargs): return True + @staticmethod + def is_3dtiles_json(_file): + with open(_file, "r") as _readed_file: + _file = json.loads(_readed_file.read()) + # required key described in the specification of 3dtiles + # https://docs.ogc.org/cs/22-025r4/22-025r4.html#toc92 + is_valid = all(key in _file.keys() for key in ("asset", "geometricError", "root")) + + if not is_valid: + raise Invalid3DTilesException( + "The provided 3DTiles is not valid, some of the mandatory keys are missing. Mandatory keys are: 'asset', 'geometricError', 'root'" + ) + + return _file + @staticmethod def validate_3dtile_payload(payload): # if the keys are there, let's check if the mandatory child are there too @@ -216,7 +221,7 @@ def create_geonode_resource( asset=None, ): # we want just the tileset.json as location of the asset - asset.location = [path for path in asset.location if "tileset.json" in path] + asset.location = [path for path in asset.location if path.endswith(".json")] asset.save() resource = super().create_geonode_resource(layer_name, alternate, execution_id, ResourceBase, asset) From cc1e8a918e09665fa5ee0448a1e19aa3d5c443eb Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:04:41 +0100 Subject: [PATCH 35/61] Update .clabot (#12838) add cmotadev to .clabot --- .clabot | 1 + 1 file changed, 1 insertion(+) diff --git a/.clabot b/.clabot index 2e706ddd684..ce0375d95bb 100644 --- a/.clabot +++ b/.clabot @@ -78,6 +78,7 @@ "fvicent", "RegisSinjari", "Gpetrak", + "cmotadev", "kilichenko-pixida" ] } From 1a6108be5230c459cdd1775ff7824a1caaa7cf80 Mon Sep 17 00:00:00 2001 From: George Petrakis Date: Tue, 21 Jan 2025 14:14:11 +0200 Subject: [PATCH 36/61] [FIXES #12766] API for timeseries settings (#12767) * adding timeseries API * fixing a bug * black re-formatting * Fix serializer for DatasetTimeSerie * make some small improvements * black reformatting * improving the code * formatting the code * adding get_choices under the __init__ function of the serializer * adding a support_time property * update layers/views with the support_time property * rename the property support_time to supports_time * adding a get_choices property to the Dataset model and extending the supports_time property * adding tests for the get_time_info function and for the supports_time property * fixing a bug * update the tests for the get_time_info * removing non-used module --------- Co-authored-by: Mattia --- geonode/geoserver/helpers.py | 31 +++++ geonode/geoserver/tests/test_helpers.py | 87 +++++++++++++- geonode/layers/api/serializers.py | 37 ++++++ geonode/layers/api/views.py | 106 ++++++++++++++++++ geonode/layers/models.py | 15 +++ geonode/layers/tests.py | 38 ++++++- geonode/layers/views.py | 27 +---- .../0050_alter_uploadsizelimit_max_size.py | 20 ++++ 8 files changed, 336 insertions(+), 25 deletions(-) create mode 100644 geonode/upload/migrations/0050_alter_uploadsizelimit_max_size.py diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index a152e4ab1bb..d28439f7854 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -1808,6 +1808,37 @@ def set_time_info(layer, attribute, end_attribute, presentation, precision_value gs_catalog.save(resource) +def get_time_info(layer): + """ + Get the time configuration for a layer + """ + time_info = {} + gs_layer = gs_catalog.get_layer(name=layer.name) + if gs_layer is not None: + gs_time_info = gs_layer.resource.metadata.get("time") + if gs_time_info.enabled: + _attr = layer.attributes.filter(attribute=gs_time_info.attribute).first() + time_info["attribute"] = _attr.pk if _attr else None + if gs_time_info.end_attribute is not None: + end_attr = layer.attributes.filter(attribute=gs_time_info.end_attribute).first() + time_info["end_attribute"] = end_attr.pk if end_attr else None + time_info["presentation"] = gs_time_info.presentation + lookup_value = sorted(list(gs_time_info._lookup), key=lambda x: x[1], reverse=True) + if gs_time_info.resolution is not None: + res = gs_time_info.resolution // 1000 + for el in lookup_value: + if res % el[1] == 0: + time_info["precision_value"] = res // el[1] + time_info["precision_step"] = el[0] + break + else: + time_info["precision_value"] = gs_time_info.resolution + time_info["precision_step"] = "seconds" + return time_info + else: + return None + + ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"] _wms = None diff --git a/geonode/geoserver/tests/test_helpers.py b/geonode/geoserver/tests/test_helpers.py index 2fb1b3c6754..1c32898d6b1 100644 --- a/geonode/geoserver/tests/test_helpers.py +++ b/geonode/geoserver/tests/test_helpers.py @@ -31,6 +31,10 @@ from geonode.layers.populate_datasets_data import create_dataset_data from geonode.tests.base import GeoNodeBaseTestSupport from geonode.geoserver.views import _response_callback +from geonode.layers.models import Dataset, Attribute +from uuid import uuid4 +from django.contrib.auth import get_user_model + from geonode.geoserver.helpers import ( gs_catalog, ows_endpoint_in_path, @@ -38,8 +42,10 @@ extract_name_from_sld, get_dataset_capabilities_url, get_layer_ows_url, + get_time_info, ) from geonode.geoserver.ows import _wcs_link, _wfs_link, _wms_link +from unittest.mock import patch, Mock logger = logging.getLogger(__name__) @@ -267,7 +273,6 @@ def test_dataset_capabilties_url(self): @on_ogc_backend(geoserver.BACKEND_PACKAGE) def test_layer_ows_url(self): - from geonode.layers.models import Dataset ows_url = settings.GEOSERVER_PUBLIC_LOCATION identifier = "geonode:CA" @@ -275,3 +280,83 @@ def test_layer_ows_url(self): expected_url = f"{ows_url}geonode/CA/ows" capabilities_url = get_layer_ows_url(dataset) self.assertEqual(capabilities_url, expected_url, capabilities_url) + + # Tests for geonode.geoserver.helpers.get_time_info + @patch("geonode.geoserver.helpers.gs_catalog") + def test_get_time_info_valid_layer(self, mock_gs_catalog): + + mock_dataset = Dataset.objects.create( + uuid=str(uuid4()), + owner=get_user_model().objects.get(username=self.user), + name="geonode:states", + store="httpfooremoteservce", + subtype="remote", + alternate="geonode:states", + ) + + Attribute.objects.create(pk=5, attribute="begin", dataset_id=mock_dataset.pk) + + Attribute.objects.create(pk=6, attribute="end", dataset_id=mock_dataset.pk) + + # Build mock GeoServer's time info + mock_gs_time_info = Mock() + mock_gs_time_info.enabled = True + mock_gs_time_info.attribute = "begin" + mock_gs_time_info.end_attribute = "end" + mock_gs_time_info.presentation = "DISCRETE_INTERVAL" + mock_gs_time_info.resolution = 5000 + mock_gs_time_info._lookup = [("seconds", 1), ("minutes", 60)] + + mock_gs_layer = Mock() + mock_gs_layer.resource.metadata.get.return_value = mock_gs_time_info + mock_gs_catalog.get_layer.return_value = mock_gs_layer + + result = get_time_info(mock_dataset) + + self.assertEqual(result["attribute"], 5) + self.assertEqual(result["end_attribute"], 6) + self.assertEqual(result["presentation"], "DISCRETE_INTERVAL") + self.assertEqual(result["precision_value"], 5) + self.assertEqual(result["precision_step"], "seconds") + + @patch("geonode.geoserver.helpers.gs_catalog") + def test_get_time_info_with_time_disabled(self, mock_gs_catalog): + + mock_dataset = Dataset.objects.create( + uuid=str(uuid4()), + owner=get_user_model().objects.get(username=self.user), + name="geonode:states", + store="httpfooremoteservce", + subtype="remote", + alternate="geonode:states", + ) + + Attribute.objects.create(pk=5, attribute="begin", dataset_id=mock_dataset.pk) + + Attribute.objects.create(pk=6, attribute="end", dataset_id=mock_dataset.pk) + + mock_gs_time_info = Mock() + mock_gs_time_info.enabled = False + mock_gs_time_info.attribute = "begin" + mock_gs_time_info.end_attribute = "end" + mock_gs_time_info.presentation = "DISCRETE_INTERVAL" + mock_gs_time_info.resolution = 10000 + mock_gs_time_info._lookup = [("seconds", 1), ("minutes", 60)] + + mock_gs_layer = Mock() + mock_gs_layer.resource.metadata.get.return_value = mock_gs_time_info + mock_gs_catalog.get_layer.return_value = mock_gs_layer + + result = get_time_info(mock_dataset) + self.assertEqual(result, {}) + + @patch("geonode.geoserver.helpers.gs_catalog") + def test_get_time_info_no_layer(self, mock_gs_catalog): + + mock_gs_catalog.get_layer.return_value = None + + mock_layer = Mock() + mock_layer.name = "nonexistent_layer" + + result = get_time_info(mock_layer) + self.assertIsNone(result) diff --git a/geonode/layers/api/serializers.py b/geonode/layers/api/serializers.py index ab5a2bc2e00..9df1f8c57bb 100644 --- a/geonode/layers/api/serializers.py +++ b/geonode/layers/api/serializers.py @@ -213,3 +213,40 @@ class DatasetMetadataSerializer(serializers.Serializer): class Meta: fields = "metadata_file" + + +class DatasetTimeSeriesSerializer(serializers.Serializer): + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + layer = self.context.get("layer") + + if layer: + # use the get_choices method of the Dataset model + choices = [(None, "-----")] + layer.get_choices + self.fields["attribute"].choices = choices + self.fields["end_attribute"].choices = choices + else: + choices = [(None, "-----")] + + has_time = serializers.BooleanField(default=False) + attribute = serializers.ChoiceField(choices=[], required=False) + end_attribute = serializers.ChoiceField(choices=[], required=False) + presentation = serializers.ChoiceField( + required=False, + choices=[ + ("LIST", "List of all the distinct time values"), + ("DISCRETE_INTERVAL", "Intervals defined by the resolution"), + ( + "CONTINUOUS_INTERVAL", + "Continuous Intervals for data that is frequently updated, resolution describes the frequency of updates", + ), + ], + ) + precision_value = serializers.IntegerField(required=False) + precision_step = serializers.ChoiceField( + required=False, + choices=[("years",) * 2, ("months",) * 2, ("days",) * 2, ("hours",) * 2, ("minutes",) * 2, ("seconds",) * 2], + ) diff --git a/geonode/layers/api/views.py b/geonode/layers/api/views.py index 35fdbc2f18b..51c918a9c4e 100644 --- a/geonode/layers/api/views.py +++ b/geonode/layers/api/views.py @@ -40,6 +40,8 @@ from geonode.resource.utils import update_resource from geonode.resource.manager import resource_manager from rest_framework.exceptions import NotFound +from django.shortcuts import get_object_or_404 +from django.http import JsonResponse from geonode.storage.manager import StorageManager @@ -47,9 +49,16 @@ DatasetSerializer, DatasetListSerializer, DatasetMetadataSerializer, + DatasetTimeSeriesSerializer, ) from .permissions import DatasetPermissionsFilter +from geonode import geoserver +from geonode.utils import check_ogc_backend + +if check_ogc_backend(geoserver.BACKEND_PACKAGE): + from geonode.geoserver.helpers import get_time_info + import logging logger = logging.getLogger(__name__) @@ -80,6 +89,8 @@ class DatasetViewSet(ApiPresetsInitializer, DynamicModelViewSet, AdvertisedListM def get_serializer_class(self): if self.action == "list": return DatasetListSerializer + if self.action == "timeseries_info": + return DatasetTimeSeriesSerializer return DatasetSerializer def partial_update(self, request, *args, **kwargs): @@ -187,3 +198,98 @@ def maps(self, request, pk=None, *args, **kwargs): dataset = self.get_object() resources = dataset.maps return Response(SimpleMapSerializer(many=True).to_representation(resources)) + + @action( + detail=True, + url_path="timeseries", + url_name="timeseries", + methods=["get", "put"], + permission_classes=[IsAuthenticated], + ) + def timeseries_info(self, request, pk, *args, **kwards): + """ + Endpoint for timeseries information + + url = "http://localhost:8080/api/v2/datasets/{dataset_id}/timeseries" + + cURL examples: + GET method + curl -X GET http://localhost:8000/api/v2/datasets/1/timeseries -u : + + PUT method + curl -X PUT http://localhost:8000/api/v2/datasets/1/timeseries -u : + -H "Content-Type: application/json" -d '{"has_time": true, "attribute": 4, "end_attribute": 5, + "presentation": "DISCRETE_INTERVAL", "precision_value": 2, "precision_step": "months"}' + """ + + layer = get_object_or_404(Dataset, id=pk) + + if layer.supports_time is False: + return JsonResponse({"message": "The time dimension is not supported for this dataset."}, status=200) + + if request.method == "GET": + + time_info = get_time_info(layer) + serializer = DatasetTimeSeriesSerializer(data=time_info, context={"layer": layer}) + serializer.is_valid(raise_exception=True) + serialized_time_info = serializer.data + + if layer.has_time is True and time_info is not None: + serialized_time_info["has_time"] = layer.has_time + return JsonResponse(serialized_time_info, status=200) + else: + return JsonResponse({"message": "No time information available."}, status=404) + + if request.method == "PUT": + + serializer = DatasetTimeSeriesSerializer(data=request.data, context={"layer": layer}) + serializer.is_valid(raise_exception=True) + serialized_time_info = serializer.validated_data + + if serialized_time_info.get("has_time") is True: + + start_attr = ( + layer.attributes.get(pk=serialized_time_info.get("attribute")).attribute + if serialized_time_info.get("attribute") + else None + ) + end_attr = ( + layer.attributes.get(pk=serialized_time_info.get("end_attribute")).attribute + if serialized_time_info.get("end_attribute") + else None + ) + + # Save the has_time value to the database + layer.has_time = True + layer.save() + + resource_manager.exec( + "set_time_info", + None, + instance=layer, + time_info={ + "attribute": start_attr, + "end_attribute": end_attr, + "presentation": serialized_time_info.get("presentation", None), + "precision_value": serialized_time_info.get("precision_value", None), + "precision_step": serialized_time_info.get("precision_step", None), + "enabled": serialized_time_info.get("has_time", False), + }, + ) + + resource_manager.update( + layer.uuid, + instance=layer, + notify=True, + ) + return JsonResponse({"message": "the time information data was updated successfully"}, status=200) + else: + # Save the has_time value to the database + layer.has_time = False + layer.save() + + return JsonResponse( + { + "message": "The time information was not updated since the time dimension is disabled for this layer" + } + ) diff --git a/geonode/layers/models.py b/geonode/layers/models.py index 016597dcb44..2a5b4dc7baa 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -164,6 +164,21 @@ def is_vector(self): def is_raster(self): return self.subtype == "raster" + @property + def supports_time(self): + valid_attributes = self.get_choices + # check if the layer object if a vector and + # includes valid_attributes + if self.is_vector() and valid_attributes: + return True + return False + + @property + def get_choices(self): + + attributes = Attribute.objects.filter(dataset_id=self.pk) + return [(_a.pk, _a.attribute) for _a in attributes if _a.attribute_type in ["xsd:dateTime", "xsd:date"]] + @property def display_type(self): if self.subtype in ["vector", "vector_time"]: diff --git a/geonode/layers/tests.py b/geonode/layers/tests.py index d11ad69ec5f..ba94558e5b4 100644 --- a/geonode/layers/tests.py +++ b/geonode/layers/tests.py @@ -23,7 +23,7 @@ import logging from uuid import uuid4 -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch, PropertyMock from collections import namedtuple from django.urls import reverse @@ -937,6 +937,42 @@ def test_dataset_download_call_the_catalog_work_for_vector(self, pathed_template ({"alternate": layer.alternate, "download_format": "application/zip"},), pathed_template.mock_calls[1].args ) + @patch.object(Dataset, "get_choices", new_callable=PropertyMock) + def test_supports_time_with_vector_time_subtype(self, mock_get_choices): + + # set valid attributes + mock_get_choices.return_value = [(4, "timestamp"), (5, "begin"), (6, "end")] + + mock_dataset = Dataset(subtype="vector") + self.assertTrue(mock_dataset.supports_time) + + @patch.object(Dataset, "get_choices", new_callable=PropertyMock) + def test_supports_time_with_non_vector_subtype(self, mock_get_choices): + + # set valid attributes + mock_get_choices.return_value = [(4, "timestamp"), (5, "begin"), (6, "end")] + + mock_dataset = Dataset(subtype="raster") + self.assertFalse(mock_dataset.supports_time) + + @patch.object(Dataset, "get_choices", new_callable=PropertyMock) + def test_supports_time_with_vector_subtype_and_invalid_attributes(self, mock_get_choices): + + # Get an empty list from get_choices method due to invalid attributes + mock_get_choices.return_value = [] + + mock_dataset = Dataset(subtype="vector") + self.assertFalse(mock_dataset.supports_time) + + @patch.object(Dataset, "get_choices", new_callable=PropertyMock) + def test_supports_time_with_raster_subtype_and_invalid_attributes(self, mock_get_choices): + + # Get an empty list from get_choices method due to invalid attributes + mock_get_choices.return_value = [] + + mock_dataset = Dataset(subtype="raster") + self.assertFalse(mock_dataset.supports_time) + class TestLayerDetailMapViewRights(GeoNodeBaseTestSupport): fixtures = ["initial_data.json", "group_test_data.json", "default_oauth_apps.json"] diff --git a/geonode/layers/views.py b/geonode/layers/views.py index f7907326936..7012e797af7 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -63,7 +63,7 @@ from geonode.geoserver.helpers import ogc_server_settings if check_ogc_backend(geoserver.BACKEND_PACKAGE): - from geonode.geoserver.helpers import gs_catalog + from geonode.geoserver.helpers import gs_catalog, get_time_info CONTEXT_LOG_FILE = ogc_server_settings.LOG_FILE @@ -332,28 +332,9 @@ def dataset_metadata( prefix="category_choice_field", initial=topic_category.id if topic_category else None ) - gs_layer = gs_catalog.get_layer(name=layer.name) initial = {} - if gs_layer is not None and layer.has_time: - gs_time_info = gs_layer.resource.metadata.get("time") - if gs_time_info.enabled: - _attr = layer.attributes.filter(attribute=gs_time_info.attribute).first() - initial["attribute"] = _attr.pk if _attr else None - if gs_time_info.end_attribute is not None: - end_attr = layer.attributes.filter(attribute=gs_time_info.end_attribute).first() - initial["end_attribute"] = end_attr.pk if end_attr else None - initial["presentation"] = gs_time_info.presentation - lookup_value = sorted(list(gs_time_info._lookup), key=lambda x: x[1], reverse=True) - if gs_time_info.resolution is not None: - res = gs_time_info.resolution // 1000 - for el in lookup_value: - if res % el[1] == 0: - initial["precision_value"] = res // el[1] - initial["precision_step"] = el[0] - break - else: - initial["precision_value"] = gs_time_info.resolution - initial["precision_step"] = "seconds" + if layer.supports_time and layer.has_time: + initial = get_time_info(layer) timeseries_form = DatasetTimeSerieForm(instance=layer, prefix="timeseries", initial=initial) @@ -465,7 +446,7 @@ def dataset_metadata( layer.has_time = dataset_form.cleaned_data.get("has_time", layer.has_time) if ( - layer.is_vector() + layer.supports_time and timeseries_form.cleaned_data and ("has_time" in dataset_form.changed_data or timeseries_form.changed_data) ): diff --git a/geonode/upload/migrations/0050_alter_uploadsizelimit_max_size.py b/geonode/upload/migrations/0050_alter_uploadsizelimit_max_size.py new file mode 100644 index 00000000000..686590ff132 --- /dev/null +++ b/geonode/upload/migrations/0050_alter_uploadsizelimit_max_size.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.16 on 2024-12-06 08:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("upload", "0049_move_data_from_importer_to_upload"), + ] + + operations = [ + migrations.AlterField( + model_name="uploadsizelimit", + name="max_size", + field=models.PositiveBigIntegerField( + default=104857600, help_text="The maximum file size allowed for upload (bytes)." + ), + ), + ] From 12ebd8c4514be415163f2d6c90866215b2636c81 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Miranda Mota Date: Tue, 21 Jan 2025 09:15:07 -0300 Subject: [PATCH 37/61] [Fixes #12815] Add custom login view/template to support recaptcha on Login Form (#12825) (#12831) * Add custom login view/template to support recaptcha on Login Form --- .../people/{forms.py => forms/__init__.py} | 13 ------- geonode/people/forms/recaptcha.py | 39 +++++++++++++++++++ .../templates/people/account_login.html | 14 +++++++ geonode/people/views.py | 6 ++- geonode/settings.py | 5 ++- geonode/urls.py | 3 +- 6 files changed, 64 insertions(+), 16 deletions(-) rename geonode/people/{forms.py => forms/__init__.py} (86%) create mode 100644 geonode/people/forms/recaptcha.py create mode 100644 geonode/people/templates/people/account_login.html diff --git a/geonode/people/forms.py b/geonode/people/forms/__init__.py similarity index 86% rename from geonode/people/forms.py rename to geonode/people/forms/__init__.py index 9fbdf4ae638..2fa95de7bfb 100644 --- a/geonode/people/forms.py +++ b/geonode/people/forms/__init__.py @@ -24,23 +24,10 @@ from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.utils.translation import gettext_lazy as _ -try: - from captcha.fields import ReCaptchaField -except ImportError: - from django_recaptcha.fields import ReCaptchaField - # Ported in from django-registration attrs_dict = {"class": "required"} -class AllauthReCaptchaSignupForm(forms.Form): - captcha = ReCaptchaField(label=False) - - def signup(self, request, user): - """Required, or else it thorws deprecation warnings""" - pass - - class ProfileCreationForm(UserCreationForm): class Meta: model = get_user_model() diff --git a/geonode/people/forms/recaptcha.py b/geonode/people/forms/recaptcha.py new file mode 100644 index 00000000000..6acbc7ae58e --- /dev/null +++ b/geonode/people/forms/recaptcha.py @@ -0,0 +1,39 @@ +# +# Copyright (C) 2019 Open Source Geospatial Foundation - all rights reserved +# +# 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 import forms +from allauth.account.forms import LoginForm + +try: + from captcha.fields import ReCaptchaField +except ImportError: + from django_recaptcha.fields import ReCaptchaField + + +class AllauthReCaptchaSignupForm(forms.Form): + captcha = ReCaptchaField(label=False) + + def signup(self, request, user): + """Required, or else it thorws deprecation warnings""" + pass + + +class AllauthRecaptchaLoginForm(LoginForm): + captcha = ReCaptchaField(label=False) + + def login(self, *args, **kwargs): + return super(AllauthRecaptchaLoginForm, self).login(*args, **kwargs) diff --git a/geonode/people/templates/people/account_login.html b/geonode/people/templates/people/account_login.html new file mode 100644 index 00000000000..b37273684f4 --- /dev/null +++ b/geonode/people/templates/people/account_login.html @@ -0,0 +1,14 @@ +{% extends "account/login.html" %} +{% comment %} Inherited from Django AllAuth default login form {% endcomment %} + +{% block extra_script %} + +{% endblock extra_script %} diff --git a/geonode/people/views.py b/geonode/people/views.py index 3abfae18fcf..bf9e8f4cf8f 100644 --- a/geonode/people/views.py +++ b/geonode/people/views.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # ######################################################################### -from allauth.account.views import SignupView +from allauth.account.views import SignupView, LoginView from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.contrib import messages @@ -57,6 +57,10 @@ def get_context_data(self, **kwargs): return ret +class CustomLoginView(LoginView): + template_name = "people/account_login.html" + + @login_required def profile_edit(request, username=None): if username is None: diff --git a/geonode/settings.py b/geonode/settings.py index 17532826c7d..8e31414e304 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -1399,8 +1399,11 @@ if "django_recaptcha" not in INSTALLED_APPS: INSTALLED_APPS += ("django_recaptcha",) ACCOUNT_SIGNUP_FORM_CLASS = os.getenv( - "ACCOUNT_SIGNUP_FORM_CLASS", "geonode.people.forms.AllauthReCaptchaSignupForm" + "ACCOUNT_SIGNUP_FORM_CLASS", "geonode.people.forms.recaptcha.AllauthReCaptchaSignupForm" ) + + # https://docs.allauth.org/en/dev/account/configuration.html + ACCOUNT_FORMS = dict(login="geonode.people.forms.recaptcha.AllauthRecaptchaLoginForm") """ In order to generate reCaptcha keys, please see: - https://pypi.org/project/django-recaptcha/#installation diff --git a/geonode/urls.py b/geonode/urls.py index 87d489d4edd..20b93d08d21 100644 --- a/geonode/urls.py +++ b/geonode/urls.py @@ -43,7 +43,7 @@ from geonode.utils import check_ogc_backend from geonode.base import register_url_event from geonode.messaging.urls import urlpatterns as msg_urls -from .people.views import CustomSignupView +from .people.views import CustomSignupView, CustomLoginView from oauth2_provider.urls import app_name as oauth2_app_name, base_urlpatterns, oidc_urlpatterns admin.autodiscover() @@ -93,6 +93,7 @@ re_path(r"^h_keywords_api$", views.h_keywords, name="h_keywords_api"), # Social views re_path(r"^account/signup/", CustomSignupView.as_view(), name="account_signup"), + re_path(r"^account/login/", CustomLoginView.as_view(), name="account_login"), re_path(r"^account/", include("allauth.urls")), re_path(r"^invitations/", include("geonode.invitations.urls", namespace="geonode.invitations")), re_path(r"^people/", include("geonode.people.urls")), From 02589390897f0ddc0e267a339b74601a7bfe99dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:46:17 +0100 Subject: [PATCH 38/61] build(deps): bump django from 4.2.16 to 4.2.18 (#12827) * build(deps): bump django from 4.2.16 to 4.2.18 Bumps [django](https://github.com/django/django) from 4.2.16 to 4.2.18. - [Commits](https://github.com/django/django/compare/4.2.16...4.2.18) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] * fix setup.cfg --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mattia --- requirements.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c6b4132b859..96877d037a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Pillow==10.4.0 lxml==5.2.1 psycopg2==2.9.9 -Django==4.2.16 +Django==4.2.18 # Other amqp==5.2.0 diff --git a/setup.cfg b/setup.cfg index dd2555fcfd8..3c0ef6f979c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ install_requires = Pillow==10.4.0 lxml==5.2.1 psycopg2==2.9.9 - Django==4.2.16 + Django==4.2.18 # Other amqp==5.2.0 From 58b43b05239feb33d310e580e66633fd4abedaf9 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Miranda Mota Date: Mon, 27 Jan 2025 06:10:04 -0300 Subject: [PATCH 39/61] Add avatar templates, changing default from django-avatar to geonode-base (#12842) --- .../people/templates/people/avatar/add.html | 27 ++++++++++++ .../templates/people/avatar/change.html | 42 +++++++++++++++++++ .../people/avatar/confirm_delete.html | 26 ++++++++++++ geonode/settings.py | 5 +++ 4 files changed, 100 insertions(+) create mode 100644 geonode/people/templates/people/avatar/add.html create mode 100644 geonode/people/templates/people/avatar/change.html create mode 100644 geonode/people/templates/people/avatar/confirm_delete.html diff --git a/geonode/people/templates/people/avatar/add.html b/geonode/people/templates/people/avatar/add.html new file mode 100644 index 00000000000..70f74f35e47 --- /dev/null +++ b/geonode/people/templates/people/avatar/add.html @@ -0,0 +1,27 @@ +{% extends "geonode_base.html" %} +{% load i18n avatar_tags bootstrap_tags %} + +{% block body_outer %} + +
+
+

{% trans "Your current avatar: " %}

+ {% avatar user %} + {% if not avatars %} +

{% trans "You haven't uploaded an avatar yet. Please upload one now." %}

+ {% endif %} +
+

{% trans "Add new avatar" %}

+
+ {{ upload_avatar_form|as_bootstrap_inline }} +

+ {% csrf_token %} + + {% trans "Go back to Edit Your Profile" %} +

+
+
+
+{% endblock %} diff --git a/geonode/people/templates/people/avatar/change.html b/geonode/people/templates/people/avatar/change.html new file mode 100644 index 00000000000..abbaa540cad --- /dev/null +++ b/geonode/people/templates/people/avatar/change.html @@ -0,0 +1,42 @@ +{% extends "geonode_base.html" %} +{% load i18n avatar_tags bootstrap_tags %} + +{% block body_outer %} + + +
+
+

{% trans "Your current avatar: " %}

+ {% avatar user %} + {% if not avatars %} +

{% trans "You haven't uploaded an avatar yet. Please upload one now." %}

+ {% else %} +
+

{% trans "Select default avatar"%}

+
+
    + {{ primary_avatar_form|as_bootstrap_inline }} +
+

+ {% csrf_token %} + + {% trans "Delete avatar" %} + {% trans "Go back to Edit Your Profile" %} +

+
+ {% endif %} +
+

{% trans "Add new avatar" %}

+
+ {{ upload_avatar_form|as_bootstrap_inline }} +

+ {% csrf_token %} + + {% trans "Go back to Edit Your Profile" %} +

+
+
+
+{% endblock %} diff --git a/geonode/people/templates/people/avatar/confirm_delete.html b/geonode/people/templates/people/avatar/confirm_delete.html new file mode 100644 index 00000000000..679a1e1a2eb --- /dev/null +++ b/geonode/people/templates/people/avatar/confirm_delete.html @@ -0,0 +1,26 @@ +{% extends "geonode_base.html" %} +{% load i18n bootstrap_tags %} + +{% block body_outer %} + +
+
+ {% if not avatars %} + {% url 'avatar_change' as avatar_change_url %} +

{% blocktrans %}You have no avatars to delete. Please upload one now.{% endblocktrans %}

+ {% else %} +

{% trans "Please select the avatars that you would like to delete." %}

+
+ {{ delete_avatar_form|as_bootstrap_inline }} +

+ {% csrf_token %} + + {% trans "Go back to Edit Your Profile" %} +

+
+ {% endif %} +
+
+{% endblock %} diff --git a/geonode/settings.py b/geonode/settings.py index 8e31414e304..a4c27949c86 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -2331,3 +2331,8 @@ def get_geonode_catalogue_service(): ] INSTALLED_APPS += ("geonode.assets",) GEONODE_APPS += ("geonode.assets",) + +# Django-Avatar - Change default templates to Geonode based +AVATAR_ADD_TEMPLATE = "people/avatar/add.html" +AVATAR_CHANGE_TEMPLATE = "people/avatar/change.html" +AVATAR_DELETE_TEMPLATE = "people/avatar/confirm_delete.html" From 5eaad8a8f05291c1383756e64f665e608b021666 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Mon, 27 Jan 2025 14:12:34 +0100 Subject: [PATCH 40/61] [Fixes #12847] Stop using Gravatar service for the default gravar (#12849) * disable gravatar provider * fix hardcoded avatar urls in permspec tests * fix hardcoded avatar urls in permspec tests (2) --- geonode/base/api/tests.py | 31 ++++++++++--------- .../templates/people/profile_detail.html | 2 +- .../people/templates/people/profile_edit.html | 2 +- geonode/security/tests.py | 21 +++++++------ geonode/settings.py | 1 - geonode/templates/avatar/avatar_tag.html | 1 + 6 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 geonode/templates/avatar/avatar_tag.html diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index e318fd06e61..e1cc1f6f3a7 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -42,6 +42,7 @@ from django.contrib.auth import get_user_model from owslib.etree import etree +from avatar.templatetags.avatar_tags import avatar_url from rest_framework.test import APITestCase from rest_framework.renderers import JSONRenderer @@ -1030,13 +1031,13 @@ def test_perms_resources(self): "username": bobby.username, "first_name": bobby.first_name, "last_name": bobby.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "owner", "is_staff": False, "is_superuser": False, }, { - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "first_name": "admin", "id": 1, "last_name": "", @@ -1111,7 +1112,7 @@ def test_perms_resources(self): "username": bobby.username, "first_name": bobby.first_name, "last_name": bobby.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "owner", "is_staff": False, "is_superuser": False, @@ -1121,13 +1122,13 @@ def test_perms_resources(self): "username": norman.username, "first_name": norman.first_name, "last_name": norman.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "edit", "is_staff": False, "is_superuser": False, }, { - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "first_name": "admin", "id": 1, "last_name": "", @@ -1160,13 +1161,13 @@ def test_perms_resources(self): "username": bobby.username, "first_name": bobby.first_name, "last_name": bobby.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "owner", "is_staff": False, "is_superuser": False, }, { - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "first_name": "admin", "id": 1, "last_name": "", @@ -1223,13 +1224,13 @@ def test_perms_resources(self): "username": bobby.username, "first_name": bobby.first_name, "last_name": bobby.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "owner", "is_staff": False, "is_superuser": False, }, { - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "first_name": "admin", "id": 1, "last_name": "", @@ -2156,7 +2157,7 @@ def test_manager_can_edit_map(self): "username": "bobby", "first_name": "bobby", "last_name": "", - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "manage", "is_superuser": False, "is_staff": False, @@ -2166,7 +2167,7 @@ def test_manager_can_edit_map(self): "username": "admin", "first_name": "admin", "last_name": "", - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "owner", "is_superuser": True, "is_staff": True, @@ -2197,7 +2198,7 @@ def test_manager_can_edit_map(self): "username": "bobby", "first_name": "bobby", "last_name": "", - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "manage", "is_staff": False, "is_superuser": False, @@ -2207,7 +2208,7 @@ def test_manager_can_edit_map(self): "username": "admin", "first_name": "admin", "last_name": "", - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "owner", "is_staff": True, "is_superuser": True, @@ -2236,7 +2237,7 @@ def test_manager_can_edit_map(self): "username": "bobby", "first_name": "bobby", "last_name": "", - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "manage", "is_staff": False, "is_superuser": False, @@ -2246,7 +2247,7 @@ def test_manager_can_edit_map(self): "username": "admin", "first_name": "admin", "last_name": "", - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(bobby)), "permissions": "owner", "is_staff": True, "is_superuser": True, diff --git a/geonode/people/templates/people/profile_detail.html b/geonode/people/templates/people/profile_detail.html index 5c60c9599a7..e18d623f2d6 100644 --- a/geonode/people/templates/people/profile_detail.html +++ b/geonode/people/templates/people/profile_detail.html @@ -26,7 +26,7 @@

{{ profile.name_long }}

- {% autoescape off %}{% avatar profile 240 %}{% endautoescape %} + {% autoescape off %}{% avatar profile %}{% endautoescape %}
diff --git a/geonode/people/templates/people/profile_edit.html b/geonode/people/templates/people/profile_edit.html index 90332a50566..84b772f8027 100644 --- a/geonode/people/templates/people/profile_edit.html +++ b/geonode/people/templates/people/profile_edit.html @@ -15,7 +15,7 @@

{% trans "Edit Profile for" %} {{ profile.username }}

- {% autoescape off %}{% avatar profile.username 240 %}{% endautoescape %} + {% autoescape off %}{% avatar profile.username %}{% endautoescape %} {% if user == profile %}

{% trans "Change your avatar" %}

{% endif %} diff --git a/geonode/security/tests.py b/geonode/security/tests.py index 2c634ecabea..79045951d72 100644 --- a/geonode/security/tests.py +++ b/geonode/security/tests.py @@ -29,6 +29,7 @@ from requests.auth import HTTPBasicAuth from tastypie.test import ResourceTestCaseMixin +from avatar.templatetags.avatar_tags import avatar_url from django.db.models import Q from django.urls import reverse @@ -48,7 +49,7 @@ from geonode.documents.models import Document from geonode.compat import ensure_string from geonode.upload.models import ResourceHandlerInfo -from geonode.utils import check_ogc_backend +from geonode.utils import check_ogc_backend, build_absolute_uri from geonode.tests.utils import check_dataset from geonode.decorators import on_ogc_backend from geonode.resource.manager import resource_manager @@ -1363,13 +1364,13 @@ def test_perm_spec_conversion(self): "username": standard_user.username, "first_name": standard_user.first_name, "last_name": standard_user.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "permissions": "owner", "is_staff": False, "is_superuser": False, }, { - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "first_name": "admin", "id": 1, "last_name": "", @@ -1416,13 +1417,13 @@ def test_perm_spec_conversion(self): "username": standard_user.username, "first_name": standard_user.first_name, "last_name": standard_user.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "permissions": "owner", "is_staff": False, "is_superuser": False, }, { - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "first_name": "admin", "id": 1, "last_name": "", @@ -1486,7 +1487,7 @@ def test_perm_spec_conversion(self): "username": standard_user.username, "first_name": standard_user.first_name, "last_name": standard_user.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "permissions": "view", } ] @@ -1566,13 +1567,13 @@ def test_perm_spec_conversion(self): "username": standard_user.username, "first_name": standard_user.first_name, "last_name": standard_user.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "permissions": "download", "is_staff": False, "is_superuser": False, }, { - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "first_name": "admin", "id": 1, "last_name": "", @@ -1625,13 +1626,13 @@ def test_perm_spec_conversion(self): "username": standard_user.username, "first_name": standard_user.first_name, "last_name": standard_user.last_name, - "avatar": "https://www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "permissions": "view", "is_staff": False, "is_superuser": False, }, { - "avatar": "https://www.gravatar.com/avatar/7a68c67c8d409ff07e42aa5d5ab7b765/?s=240", + "avatar": build_absolute_uri(avatar_url(standard_user)), "first_name": "admin", "id": 1, "last_name": "", diff --git a/geonode/settings.py b/geonode/settings.py index a4c27949c86..1f39f422bbb 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -1360,7 +1360,6 @@ AVATAR_PROVIDERS = ( ( "avatar.providers.PrimaryAvatarProvider", - "avatar.providers.GravatarAvatarProvider", "avatar.providers.DefaultAvatarProvider", ) if os.getenv("AVATAR_PROVIDERS") is None diff --git a/geonode/templates/avatar/avatar_tag.html b/geonode/templates/avatar/avatar_tag.html new file mode 100644 index 00000000000..e8e7a90b6a1 --- /dev/null +++ b/geonode/templates/avatar/avatar_tag.html @@ -0,0 +1 @@ + \ No newline at end of file From 6bc698f8435989dacab9ac148f90998c92e8fece Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Tue, 28 Jan 2025 18:44:38 +0100 Subject: [PATCH 41/61] Fixed missing translations for facets labels (#12859) --- geonode/facets/providers/category.py | 3 ++- geonode/facets/providers/group.py | 4 +++- geonode/facets/providers/keyword.py | 3 ++- geonode/facets/providers/region.py | 3 ++- geonode/facets/providers/users.py | 3 ++- geonode/locale/de/LC_MESSAGES/django.mo | Bin 156881 -> 156946 bytes geonode/locale/de/LC_MESSAGES/django.po | 6 ++++++ geonode/locale/es/LC_MESSAGES/django.mo | Bin 142264 -> 142363 bytes geonode/locale/es/LC_MESSAGES/django.po | 6 ++++++ geonode/locale/fr/LC_MESSAGES/django.mo | Bin 161262 -> 161365 bytes geonode/locale/fr/LC_MESSAGES/django.po | 6 ++++++ geonode/locale/it/LC_MESSAGES/django.mo | Bin 161471 -> 161538 bytes geonode/locale/it/LC_MESSAGES/django.po | 18 ++++++++++++------ geonode/locale/pt_BR/LC_MESSAGES/django.mo | Bin 182273 -> 182436 bytes geonode/locale/pt_BR/LC_MESSAGES/django.po | 6 ++++++ 15 files changed, 47 insertions(+), 11 deletions(-) diff --git a/geonode/facets/providers/category.py b/geonode/facets/providers/category.py index 488d5ab2e41..56bbea417fb 100644 --- a/geonode/facets/providers/category.py +++ b/geonode/facets/providers/category.py @@ -20,6 +20,7 @@ import logging from django.db.models import Count +from django.utils.translation import gettext_lazy as _ from geonode.base.models import TopicCategory from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_CATEGORY @@ -40,7 +41,7 @@ def get_info(self, lang="en", **kwargs) -> dict: return { "name": self.name, "filter": "filter{category.identifier.in}", - "label": "Category", + "label": _("Category"), "type": FACET_TYPE_CATEGORY, } diff --git a/geonode/facets/providers/group.py b/geonode/facets/providers/group.py index c8de489b461..fe9592a566d 100644 --- a/geonode/facets/providers/group.py +++ b/geonode/facets/providers/group.py @@ -20,7 +20,9 @@ import logging +from django.utils.translation import gettext_lazy as _ from django.db.models import Count + from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_GROUP from geonode.groups.models import GroupProfile from geonode.security.utils import get_user_visible_groups @@ -41,7 +43,7 @@ def get_info(self, lang="en", **kwargs) -> dict: return { "name": self.name, "filter": "filter{group.in}", - "label": "Group", + "label": _("Group"), "type": FACET_TYPE_GROUP, } diff --git a/geonode/facets/providers/keyword.py b/geonode/facets/providers/keyword.py index 476031f9e31..94d9bf2a875 100644 --- a/geonode/facets/providers/keyword.py +++ b/geonode/facets/providers/keyword.py @@ -20,6 +20,7 @@ import logging from django.db.models import Count +from django.utils.translation import gettext_lazy as _ from geonode.base.models import HierarchicalKeyword from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_KEYWORD @@ -40,7 +41,7 @@ def get_info(self, lang="en", **kwargs) -> dict: return { "name": self.name, "filter": "filter{keywords.slug.in}", - "label": "Keyword", + "label": _("Keyword"), "type": FACET_TYPE_KEYWORD, } diff --git a/geonode/facets/providers/region.py b/geonode/facets/providers/region.py index 73a7e578d37..6b59ac06782 100644 --- a/geonode/facets/providers/region.py +++ b/geonode/facets/providers/region.py @@ -20,6 +20,7 @@ import logging from django.db.models import Count +from django.utils.translation import gettext_lazy as _ from geonode.base.models import Region from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_PLACE @@ -40,7 +41,7 @@ def get_info(self, lang="en", **kwargs) -> dict: return { "name": self.name, "filter": "filter{regions.code.in}", - "label": "Region", + "label": _("Region"), "type": FACET_TYPE_PLACE, } diff --git a/geonode/facets/providers/users.py b/geonode/facets/providers/users.py index ac65d9ee79b..baa4877d660 100644 --- a/geonode/facets/providers/users.py +++ b/geonode/facets/providers/users.py @@ -19,6 +19,7 @@ import logging +from django.utils.translation import gettext_lazy as _ from django.contrib.auth import get_user_model from django.db.models import Count @@ -40,7 +41,7 @@ def get_info(self, lang="en", **kwargs) -> dict: return { "name": "owner", "filter": "filter{owner.pk.in}", - "label": "Owner", + "label": _("Owner"), "type": FACET_TYPE_USER, } diff --git a/geonode/locale/de/LC_MESSAGES/django.mo b/geonode/locale/de/LC_MESSAGES/django.mo index 64672f7c4586b3ef1c53db66915e69d3c57c4ec0..69bc71daea9b86ebf7d1f3bbfd25cb95cc8046c6 100644 GIT binary patch delta 32154 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 T`fWaW&8vRM=AdZatDF80Wy4>Z delta 32141 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 delta 29462 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 W9geK-zO!DqD`BRcEn~aKwEaJf880CK delta 32570 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%0D56ZoPzpl6)A;}GtGy;Bf{;>$dUlF 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 H0d4*t+aEH3 diff --git a/geonode/locale/fr/LC_MESSAGES/django.po b/geonode/locale/fr/LC_MESSAGES/django.po index 2dd120c69d2..250a4e1f952 100644 --- a/geonode/locale/fr/LC_MESSAGES/django.po +++ b/geonode/locale/fr/LC_MESSAGES/django.po @@ -199,6 +199,9 @@ msgstr "" "Une liste de mots-clés séparés par des espaces ou des virgules. Utilisez le " "widget pour choisir à partir de l'arbre hiérarchique." +msgid "Region" +msgstr "Région" + msgid "Regions" msgstr "Régions" @@ -359,6 +362,9 @@ msgstr "DOI" msgid "maintenance frequency" msgstr "fréquence des mises à jour" +msgid "Keyword" +msgstr "Mot-clés" + msgid "keywords" msgstr "mots-clés" diff --git a/geonode/locale/it/LC_MESSAGES/django.mo b/geonode/locale/it/LC_MESSAGES/django.mo index 1df6d3c5a4d4f9a581d2e68703425fabeab46446..e825e748434f552a494a02d695f507f01a125137 100644 GIT binary patch delta 33451 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 diff --git a/geonode/locale/it/LC_MESSAGES/django.po b/geonode/locale/it/LC_MESSAGES/django.po index 791d72d9319..48ebda7c4d9 100644 --- a/geonode/locale/it/LC_MESSAGES/django.po +++ b/geonode/locale/it/LC_MESSAGES/django.po @@ -39,7 +39,7 @@ msgstr "" "Project-Id-Version: GeoNode\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-08 09:02+0300\n" -"PO-Revision-Date: 2023-10-11 11:47+0200\n" +"PO-Revision-Date: 2025-01-28 17:51+0100\n" "Last-Translator: Julien Collaer \n" "Language-Team: Italian (http://www.transifex.com/geonode/geonode/language/it/)\n" "Language: it\n" @@ -47,7 +47,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.0.1\n" +"X-Generator: Poedit 3.5\n" msgid "Request to download a resource" msgstr "Richiedi di scaricare la risorsa" @@ -202,6 +202,9 @@ msgstr "" "Uno spazio o un elenco delimitato da virgole di parole chiave. Utilizzare il widget per " "selezionare dalla struttura ad albero gerarchica." +msgid "Region" +msgstr "Regione" + msgid "Regions" msgstr "Regioni" @@ -353,6 +356,9 @@ msgstr "DOI" msgid "maintenance frequency" msgstr "frequenza di revisione" +msgid "Keyword" +msgstr "Parola chiave" + msgid "keywords" msgstr "parole chiave" @@ -949,8 +955,8 @@ msgstr "Collegamento a" msgid "Related resources" msgstr "Risorse collegate" -#msgstr "Risorse correlate" +# msgstr "Risorse correlate" msgid "File" msgstr "File" @@ -6340,6 +6346,9 @@ msgstr "Livello di permessi non valido." msgid "Permission Denied" msgstr "Permesso negato" +msgid "All resources" +msgstr "Tutte le risorse" + #~ msgid "Layer name" #~ msgstr "Nome livello" @@ -6489,6 +6498,3 @@ msgstr "Permesso negato" #~ msgid "Thesaurus" #~ msgstr "Thesaurus" - -msgid "All resources" -msgstr "Tutte le risorse" \ No newline at end of file diff --git a/geonode/locale/pt_BR/LC_MESSAGES/django.mo b/geonode/locale/pt_BR/LC_MESSAGES/django.mo index 8031a3fd5b21cc46f2779a94d52d92a80ab13763..7293425f125a5f202fd52b9ad07de0499f69671b 100644 GIT binary patch delta 35196 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 Z(_SsNPtNU?DVx{Q&f7OO_Nw0Ie*n+wD4PHP delta 35149 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 B{Luga diff --git a/geonode/locale/pt_BR/LC_MESSAGES/django.po b/geonode/locale/pt_BR/LC_MESSAGES/django.po index 2c7ff22504f..55ac8506bda 100644 --- a/geonode/locale/pt_BR/LC_MESSAGES/django.po +++ b/geonode/locale/pt_BR/LC_MESSAGES/django.po @@ -181,6 +181,9 @@ msgstr "" "Uma lista de palavras-chave separadas por vírgula ou espaço. Utilize o widget para selecionar " "a partir da árvore hierárquica." +msgid "Region" +msgstr "Região" + msgid "Regions" msgstr "Regiões" @@ -328,6 +331,9 @@ msgstr "DOI" msgid "maintenance frequency" msgstr "frequência de manutenção" +msgid "Keyword" +msgstr "Palavra-chave" + msgid "keywords" msgstr "palavras-chave" From 306a958beb4ad3b66a32ecefe61f3ce405654505 Mon Sep 17 00:00:00 2001 From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:00:03 +0100 Subject: [PATCH 42/61] [Fixes #12854] Fix migrations for create handlerinfo via asset (#12856) * [Fixes #12854] Fix migrations for create handlerinfo via asset --- geonode/upload/handlers/tiles3d/handler.py | 12 +++-- .../0051__align_resourcehandler_with_asset.py | 54 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 geonode/upload/migrations/0051__align_resourcehandler_with_asset.py diff --git a/geonode/upload/handlers/tiles3d/handler.py b/geonode/upload/handlers/tiles3d/handler.py index c80511b38b1..ee6f1f6f61c 100755 --- a/geonode/upload/handlers/tiles3d/handler.py +++ b/geonode/upload/handlers/tiles3d/handler.py @@ -79,11 +79,15 @@ def can_handle(_data) -> bool: the handler is able to handle the file or not """ base = _data.get("base_file") - if not base: + try: + base = _data.get("base_file") + if not base: + return False + ext = base.split(".")[-1] if isinstance(base, str) else base.name.split(".")[-1] + if ext in ["json"] and Tiles3DFileHandler.is_3dtiles_json(base): + return True + except Exception: return False - ext = base.split(".")[-1] if isinstance(base, str) else base.name.split(".")[-1] - if ext in ["json"] and Tiles3DFileHandler.is_3dtiles_json(base): - return True return False @staticmethod diff --git a/geonode/upload/migrations/0051__align_resourcehandler_with_asset.py b/geonode/upload/migrations/0051__align_resourcehandler_with_asset.py new file mode 100644 index 00000000000..8122d8d2e49 --- /dev/null +++ b/geonode/upload/migrations/0051__align_resourcehandler_with_asset.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2.15 on 2022-10-04 13:03 + +import logging +from django.db import migrations +from geonode.upload.orchestrator import orchestrator +from geonode.layers.models import Dataset +from geonode.assets.utils import get_default_asset +from geonode.utils import get_allowed_extensions + +logger = logging.getLogger("django") + +def dataset_migration(apps, _): + NewResources = apps.get_model("upload", "ResourceHandlerInfo") + for old_resource in Dataset.objects.exclude( + pk__in=NewResources.objects.values_list("resource_id", flat=True) + ).exclude(subtype__in=["remote", None]): + # generating orchestrator expected data file + if old_resource.resourcehandlerinfo_set.first() is None: + if get_default_asset(old_resource): + available_choices = get_allowed_extensions() + not_main_files = ["xml", "sld", "zip", "kmz"] + base_file_choices = set(x for x in available_choices if x not in not_main_files) + output_files = dict() + for _file in get_default_asset(old_resource).location: + if _file.split(".")[-1] in base_file_choices: + output_files.update({"base_file": _file}) + break + else: + if old_resource.is_vector(): + output_files = {"base_file": "placeholder.shp"} + else: + output_files = {"base_file": "placeholder.tiff"} + + handler = orchestrator.get_handler(output_files) + if handler is None: + logger.error(f"Handler not found for resource: {old_resource}") + continue + handler.create_resourcehandlerinfo( + handler_module_path=str(handler), + resource=old_resource, + execution_id=None + ) + else: + logger.debug(f"resourcehandler info already exists for the resource") + + +class Migration(migrations.Migration): + dependencies = [ + ("upload", "0050_alter_uploadsizelimit_max_size"), + ] + + operations = [ + migrations.RunPython(dataset_migration), + ] From fbee678d1d54909dd9b52fb6ad1324a783a78c07 Mon Sep 17 00:00:00 2001 From: George Petrakis Date: Wed, 5 Feb 2025 16:46:13 +0200 Subject: [PATCH 43/61] [Fixes 12766] New fixes regarding the Timeseries API (#12853) * [fixes 12766] in Timeseries API * modifying the default of precision step * black reformat * Setting time dimension through the /copy endpoint --- geonode/layers/api/serializers.py | 8 +++--- geonode/layers/api/views.py | 12 +++++---- geonode/upload/handlers/common/vector.py | 33 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/geonode/layers/api/serializers.py b/geonode/layers/api/serializers.py index 9df1f8c57bb..5a95554c978 100644 --- a/geonode/layers/api/serializers.py +++ b/geonode/layers/api/serializers.py @@ -232,8 +232,8 @@ def __init__(self, *args, **kwargs): choices = [(None, "-----")] has_time = serializers.BooleanField(default=False) - attribute = serializers.ChoiceField(choices=[], required=False) - end_attribute = serializers.ChoiceField(choices=[], required=False) + attribute = serializers.ChoiceField(choices=[], required=False, allow_null=True, default=None) + end_attribute = serializers.ChoiceField(choices=[], required=False, allow_null=True, default=None) presentation = serializers.ChoiceField( required=False, choices=[ @@ -244,9 +244,11 @@ def __init__(self, *args, **kwargs): "Continuous Intervals for data that is frequently updated, resolution describes the frequency of updates", ), ], + default="LIST", ) - precision_value = serializers.IntegerField(required=False) + precision_value = serializers.IntegerField(required=False, allow_null=True) precision_step = serializers.ChoiceField( required=False, choices=[("years",) * 2, ("months",) * 2, ("days",) * 2, ("hours",) * 2, ("minutes",) * 2, ("seconds",) * 2], + default="seconds", ) diff --git a/geonode/layers/api/views.py b/geonode/layers/api/views.py index 51c918a9c4e..4beac3a89a6 100644 --- a/geonode/layers/api/views.py +++ b/geonode/layers/api/views.py @@ -259,6 +259,12 @@ def timeseries_info(self, request, pk, *args, **kwards): else None ) + if start_attr is None and end_attr is None: + return JsonResponse( + {"message": "Please select at least one option between the attribute and end_attribute"}, + status=200, + ) + # Save the has_time value to the database layer.has_time = True layer.save() @@ -288,8 +294,4 @@ def timeseries_info(self, request, pk, *args, **kwards): layer.has_time = False layer.save() - return JsonResponse( - { - "message": "The time information was not updated since the time dimension is disabled for this layer" - } - ) + return JsonResponse({"message": "The time dimension information for this layer was disabled"}) diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py index 2aed508cb82..5ae90d7afdd 100644 --- a/geonode/upload/handlers/common/vector.py +++ b/geonode/upload/handlers/common/vector.py @@ -54,6 +54,7 @@ from django.db.models import Q import pyproj from geonode.geoserver.security import delete_dataset_cache, set_geowebcache_invalidate_cache +from geonode.geoserver.helpers import get_time_info from geonode.upload.utils import ImporterRequestAction as ira logger = logging.getLogger("importer") @@ -774,8 +775,40 @@ def copy_geonode_resource( execution_id=str(_exec.exec_id), asset=get_default_asset(resource), ) + copy_assets_and_links(resource, target=new_resource) + + if resource.dataset.has_time is True: + + new_resource.has_time = True + new_resource.save() + + time_info = None + try: + time_info = get_time_info(resource.dataset) + except ValueError as e: + logger.info(f"Failed to retrieve time information: {e}") + + time_info["attribute"] = ( + resource.dataset.attributes.get(pk=time_info.get("attribute")).attribute + if time_info.get("attribute") + else None + ) + time_info["end_attribute"] = ( + resource.dataset.attributes.get(pk=time_info.get("end_attribute")).attribute + if time_info.get("end_attribute") + else None + ) + + resource_manager.exec( + "set_time_info", + None, + instance=new_resource, + time_info=time_info, + ) + new_resource.refresh_from_db() + return new_resource def get_ogr2ogr_task_group( From 9415e4d45cedc7d16fff5c0ecc2d4082e54a80a2 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Thu, 6 Feb 2025 11:48:15 +0100 Subject: [PATCH 44/61] [Fixes #12876] Removal of legacy scripts (#12877) * Removal of legacy scripts * restored pavement which is still required by tests * moved tests under tests folder --- .circleci/config.yml | 14 +- .pre-commit-config.yaml | 14 - .tx/config | 38 - AUTHORS | 1 + hooks/test | 3 - monitoring-cron | 3 - package/README | 87 - package/debian/README | 6 - package/debian/README.rst | 46 - package/debian/changelog | 10568 ---------------- package/debian/compat | 1 - package/debian/config | 3 - package/debian/control | 21 - package/debian/copyright | 37 - package/debian/docs | 0 package/debian/postinst | 58 - package/debian/postrm | 51 - package/debian/preinst | 41 - package/debian/prerm | 40 - package/debian/rules | 47 - package/debian/source/format | 1 - package/debian/support/config-post.sh | 60 - package/debian/support/config-pre.sh | 50 - package/geoserver/README | 7 - .../build_geonode-geoserver-ext-deb.sh | 65 - .../build_geonode-geoserver-ext-rpm.sh | 31 - package/geoserver/debian/changelog | 89 - package/geoserver/debian/compat | 1 - package/geoserver/debian/control | 20 - package/geoserver/debian/copyright | 351 - .../debian/geoserver-geonode-suite.postinst | 4 - .../debian/geoserver-geonode.postinst | 30 - package/geoserver/debian/mvn_settings.xml | 3 - package/geoserver/debian/preinst | 0 package/geoserver/debian/rules | 39 - package/geoserver/rpm/SPECS/geoserver.spec | 92 - package/install.sh | 234 - package/rpm/README.rst | 75 - package/rpm/SPECS/geonode.spec | 105 - package/rpm/SPECS/opengeo.repo | 5 - package/support/config-ubuntu.sh | 28 - package/support/geonode.admin | 18 - package/support/geonode.apache | 86 - package/support/geonode.binary | 13 - package/support/geonode.local_settings | 429 - package/support/geonode.robots | 4 - package/support/geonode.updateip | 212 - package/support/geonode.wsgi | 7 - package/support/geoserver.patch | 20 - .../jwcrypto-0.5.0-py2.py3-none-any.whl | Bin 71772 -> 0 bytes package/support/packages/lxml-3.6.2.tar.gz | Bin 4305516 -> 0 bytes .../support/packages/xmltodict-0.10.2.tar.gz | Bin 24854 -> 0 bytes pavement.py | 2 +- paver.sh | 2 - paver_dev.sh | 5 - paver_local.sh | 5 - publish.sh | 5 - scripts/cloud/README.rst | 25 - scripts/cloud/demo_site.py | 99 - scripts/cloud/ec2.py | 268 - scripts/cloud/fabfile.py | 316 - scripts/misc/align_remote_branches.sh | 17 - scripts/misc/apache2/geonode.conf.sample | 108 - scripts/misc/changepw.py | 23 - scripts/misc/cleanup_pyc.sh | 9 - scripts/misc/create_dbs.sh | 5 - scripts/misc/create_dbs_travis.sh | 31 - scripts/misc/django_backup.sh | 28 - scripts/misc/django_generate_secret_key.sh | 11 - scripts/misc/django_restore.sh | 30 - scripts/misc/docker_check.sh | 5 - scripts/misc/geoserver_server_setup.sh | 31 - scripts/misc/jetty-runner.xml | 12 - scripts/misc/nginx_integration.conf | 72 - scripts/misc/rabbitmqadmin | 1171 -- scripts/misc/update-instance | 21 - scripts/misc/upload.py | 60 - start_django_async.sh | 24 - tests/pavement.py | 1122 ++ tests/test.sh | 8 + tests/test_api_v2.sh | 4 + tests/test_csw.sh | 29 + tests/test_dev.sh | 8 + tests/test_integration.sh | 25 + tests/test_oauth2.sh | 9 + tests/test_upload.sh | 27 + 86 files changed, 1241 insertions(+), 15534 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 .tx/config delete mode 100644 hooks/test delete mode 100644 monitoring-cron delete mode 100644 package/README delete mode 100644 package/debian/README delete mode 100644 package/debian/README.rst delete mode 100644 package/debian/changelog delete mode 100644 package/debian/compat delete mode 100644 package/debian/config delete mode 100644 package/debian/control delete mode 100644 package/debian/copyright delete mode 100644 package/debian/docs delete mode 100644 package/debian/postinst delete mode 100644 package/debian/postrm delete mode 100644 package/debian/preinst delete mode 100644 package/debian/prerm delete mode 100755 package/debian/rules delete mode 100644 package/debian/source/format delete mode 100644 package/debian/support/config-post.sh delete mode 100644 package/debian/support/config-pre.sh delete mode 100644 package/geoserver/README delete mode 100755 package/geoserver/build_geonode-geoserver-ext-deb.sh delete mode 100755 package/geoserver/build_geonode-geoserver-ext-rpm.sh delete mode 100644 package/geoserver/debian/changelog delete mode 100644 package/geoserver/debian/compat delete mode 100755 package/geoserver/debian/control delete mode 100644 package/geoserver/debian/copyright delete mode 100755 package/geoserver/debian/geoserver-geonode-suite.postinst delete mode 100755 package/geoserver/debian/geoserver-geonode.postinst delete mode 100644 package/geoserver/debian/mvn_settings.xml delete mode 100755 package/geoserver/debian/preinst delete mode 100755 package/geoserver/debian/rules delete mode 100644 package/geoserver/rpm/SPECS/geoserver.spec delete mode 100755 package/install.sh delete mode 100644 package/rpm/README.rst delete mode 100644 package/rpm/SPECS/geonode.spec delete mode 100644 package/rpm/SPECS/opengeo.repo delete mode 100644 package/support/config-ubuntu.sh delete mode 100644 package/support/geonode.admin delete mode 100644 package/support/geonode.apache delete mode 100644 package/support/geonode.binary delete mode 100644 package/support/geonode.local_settings delete mode 100644 package/support/geonode.robots delete mode 100644 package/support/geonode.updateip delete mode 100644 package/support/geonode.wsgi delete mode 100644 package/support/geoserver.patch delete mode 100644 package/support/packages/jwcrypto-0.5.0-py2.py3-none-any.whl delete mode 100644 package/support/packages/lxml-3.6.2.tar.gz delete mode 100644 package/support/packages/xmltodict-0.10.2.tar.gz delete mode 100644 paver.sh delete mode 100755 paver_dev.sh delete mode 100644 paver_local.sh delete mode 100755 publish.sh delete mode 100644 scripts/cloud/README.rst delete mode 100644 scripts/cloud/demo_site.py delete mode 100644 scripts/cloud/ec2.py delete mode 100644 scripts/cloud/fabfile.py delete mode 100755 scripts/misc/align_remote_branches.sh delete mode 100644 scripts/misc/apache2/geonode.conf.sample delete mode 100755 scripts/misc/changepw.py delete mode 100755 scripts/misc/cleanup_pyc.sh delete mode 100755 scripts/misc/create_dbs.sh delete mode 100755 scripts/misc/create_dbs_travis.sh delete mode 100755 scripts/misc/django_backup.sh delete mode 100755 scripts/misc/django_generate_secret_key.sh delete mode 100755 scripts/misc/django_restore.sh delete mode 100755 scripts/misc/docker_check.sh delete mode 100755 scripts/misc/geoserver_server_setup.sh delete mode 100755 scripts/misc/jetty-runner.xml delete mode 100644 scripts/misc/nginx_integration.conf delete mode 100644 scripts/misc/rabbitmqadmin delete mode 100755 scripts/misc/update-instance delete mode 100755 scripts/misc/upload.py delete mode 100755 start_django_async.sh create mode 100644 tests/pavement.py create mode 100755 tests/test.sh create mode 100755 tests/test_api_v2.sh create mode 100755 tests/test_csw.sh create mode 100755 tests/test_dev.sh create mode 100755 tests/test_integration.sh create mode 100755 tests/test_oauth2.sh create mode 100755 tests/test_upload.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 7f485f1c73a..6be6574c9f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -104,43 +104,43 @@ workflows: codecov_name: smoke_tests load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh geonode.tests.smoke geonode.tests.test_message_notifications geonode.tests.test_rest_api geonode.tests.test_search geonode.tests.test_utils geonode.tests.test_headers + test_suite: ./tests/test.sh geonode.tests.smoke geonode.tests.test_message_notifications geonode.tests.test_rest_api geonode.tests.test_search geonode.tests.test_utils geonode.tests.test_headers - build: name: geonode_test_suite codecov_name: main_tests load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''security'\'' not in a and '\''geoserver'\'' not in a and '\''upload'\'' not in a]))") geonode.thumbs.tests geonode.people.tests geonode.people.socialaccount.providers.geonode_openid_connect.tests + test_suite: ./tests/test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''security'\'' not in a and '\''geoserver'\'' not in a and '\''upload'\'' not in a]))") geonode.thumbs.tests geonode.people.tests geonode.people.socialaccount.providers.geonode_openid_connect.tests - build: name: geonode_test_security codecov_name: security_tests load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''security'\'' in a]))") + test_suite: ./tests/test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''security'\'' in a]))") - build: name: geonode_test_gis_backend codecov_name: gis load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''geoserver'\'' in a]))") + test_suite: ./tests/test.sh $(python -c "import sys;from geonode import settings;sys.stdout.write('\'' '\''.join([a+'\''.tests'\'' for a in settings.GEONODE_APPS if '\''geoserver'\'' in a]))") - build: name: geonode_test_rest_apis codecov_name: api load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh geonode.api.tests geonode.base.api.tests geonode.layers.api.tests geonode.maps.api.tests geonode.documents.api.tests geonode.geoapps.api.tests + test_suite: ./tests/test.sh geonode.api.tests geonode.base.api.tests geonode.layers.api.tests geonode.maps.api.tests geonode.documents.api.tests geonode.geoapps.api.tests - build: name: geonode_test_csw codecov_name: csw load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh geonode.tests.csw geonode.catalogue.backends.tests + test_suite: ./tests/test.sh geonode.tests.csw geonode.catalogue.backends.tests - build: name: geonode_upload codecov_name: importer load_docker_cache: false save_docker_cache: false - test_suite: ./test.sh geonode.upload + test_suite: ./tests/test.sh geonode.upload # TODO # - build: # name: geonode_test_integration_upload diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index f6a72993d13..00000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-added-large-files -- repo: https://github.com/psf/black - rev: 23.9.1 - hooks: - - id: black diff --git a/.tx/config b/.tx/config deleted file mode 100644 index ddad00ca826..00000000000 --- a/.tx/config +++ /dev/null @@ -1,38 +0,0 @@ -[main] -host = https://www.transifex.com - -[geonode.master] -file_filter = geonode/locale//LC_MESSAGES/django.po -source_file = geonode/locale/en/LC_MESSAGES/django.po -source_lang = en -type = PO - -[geonode.javascript] -file_filter = geonode/locale//LC_MESSAGES/djangojs.po -source_file = geonode/locale/en/LC_MESSAGES/djangojs.po -source_lang = en -type = PO - -#[geonode.docs-index] -#file_filter = docs/i18n//LC_MESSAGES/index.po -#source_file = docs/i18n/en/LC_MESSAGES/index.po -#source_lang = en -#type = PO - -#[geonode.docs-organizational] -#file_filter = docs/i18n//LC_MESSAGES/organizational.po -#source_file = docs/i18n/en/LC_MESSAGES/organizational.po -#source_lang = en -#type = PO - -#[geonode.docs-reference] -#file_filter = docs/i18n//LC_MESSAGES/reference.po -#source_file = docs/i18n/en/LC_MESSAGES/reference.po -#source_lang = en -#type = PO - -#[geonode.docs-tutorials] -#file_filter = docs/i18n//LC_MESSAGES/tutorials.po -#source_file = docs/i18n/en/LC_MESSAGES/tutorials.po -#source_lang = en -#type = PO diff --git a/AUTHORS b/AUTHORS index 0f7690d8559..cf28806d1bb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,6 +43,7 @@ The PRIMARY AUTHORS are (and/or have been): * Toni Schönbuchner (t-book) * Florian Hoedt / Thünen-Institute (gannebamm) * Giovanni Allegri (giohappy) + * Mattia Giupponi (mattiagiupponi) And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- people who have submitted patches, reported bugs, added translations, helped diff --git a/hooks/test b/hooks/test deleted file mode 100644 index 8b2f24280ad..00000000000 --- a/hooks/test +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -echo "Skipping test..." diff --git a/monitoring-cron b/monitoring-cron deleted file mode 100644 index 47cca563ecc..00000000000 --- a/monitoring-cron +++ /dev/null @@ -1,3 +0,0 @@ -# */1 * * * * /usr/src/geonode/manage.sh collect_metrics -n -t xml >> /var/log/cron.log 2>&1 -# 0 * * * * /usr/src/geonode/manage.sh dispatch_metrics >> /var/log/cron.log 2>&1 -# An empty line is required at the end of this file for a valid cron file. diff --git a/package/README b/package/README deleted file mode 100644 index 00b7d9a66dd..00000000000 --- a/package/README +++ /dev/null @@ -1,87 +0,0 @@ -Installing GeoNode -================== - -The easiest way to install GeoNode is using the official packages for one of the supported Operating Systems. -Please be advised that GeoNode requires at least 4GB of RAM (6GB including swap). - -Ubuntu 12.04 ------------------------------ - -Open a terminal and run the following commands:: - - sudo add-apt-repository ppa:geonode/testing - sudo apt-get update - sudo apt-get install geonode - - -OSX, Windows and other operating systems ----------------------------------------- - -Our recommendation is to use a Virtual Machine with one of the supported Operating Systems. -If that is not an option then you could try to follow manually the steps of the install script -adjusting for paths and commands in your OS. - -Manual installation -------------------- - -This is mostly targeted to Linux based distributions, -it has only been tested in Ubuntu Linux but should work with minimal changes to the config file. - -# First you need to install the OS specific dependencies, -here is the complete list (the actual package name may vary):: - - geoserver, python3, python3-support, python3-dev, sun-java6-jre | openjdk-6-jre, tomcat6, postgresql-9.3, gcc, patch, zip, python3-pil, gdal-bin, libgeos-dev, python3-urlgrabber, python3-pastescript, gettext, postgresql-contrib, postgresql-9.3-postgis-2.1,libpq-dev, unzip, libjpeg-dev, libpng-dev, python3-gdal, libproj-dev, python3-psycopg2, apache2, libapache2-mod-wsgi, libxml2-dev, libxslt1-dev - -# Then you have to edit the config file that is in the support directory with the appropiate paths, -sample config files for Ubuntu and CentOS are distributed with the release packages. - -# After that, open a terminal and run the following command as a super user:: - - ./install.sh support/config.sh - -# To test your GeoNode installation simply type the following in your terminal:: - - geonode help - - You should also navigate to your browser window and type `http://localhost/` - -# After you have installing your GeoNode we recommend you to read the following guide to learn how to create users, -serve the site on a DNS or IP address and optimize your GeoNode. - http://docs.geonode.org/deploy/production.html - -Note for packagers -~~~~~~~~~~~~~~~~~~ - -There is an advanced flag for the install script called 'step'. -There are two main steps to install GeoNode, -the first one is to place the required files in the right places (referred to as pre-install) and -the other to create the postgis database and edit the required Django and GeoServer config files (referred to as post-install). - -By default the install script does both, but usually it is appropriate to perform the first of these steps during package creation and the second one at install time. - -The step flag supports three values: 'pre', 'post' and 'all'. Default is 'all'. Here is usage example:: - - # in debian/rules#install - ./install.sh -s pre support/config.sh - - # in debian/postinst - ./install.sh -s post support/config.sh - - -GPL License -=========== - -GeoNode is Copyright 2016 Open Source Geospatial Foundation (OSGeo). - -GeoNode 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. - -GeoNode 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 GeoNode. If not, see . diff --git a/package/debian/README b/package/debian/README deleted file mode 100644 index 80aae1f68af..00000000000 --- a/package/debian/README +++ /dev/null @@ -1,6 +0,0 @@ -The Debian Package geonode ----------------------------- - -Comments regarding the Package - - -- root Thu, 04 Nov 2010 14:49:39 -0700 diff --git a/package/debian/README.rst b/package/debian/README.rst deleted file mode 100644 index c43682f499b..00000000000 --- a/package/debian/README.rst +++ /dev/null @@ -1,46 +0,0 @@ -Debian packaging scripts for GeoNode -==================================== - -This repository contains the scripts used to build the .deb (Ubuntu) package -for GeoNode. If you are interested in modifying GeoNode itself you may find -http://github.com/GeoNode/geonode more relevant. - -Building --------- - -To produce a .deb package which can be redistributed: - -* Install the debian packaging tools:: - - apt-get install debhelper devscripts - -* Acquire a GeoNode tar.gz archive (by either building it from sources, or from - http://dev.geonode.org/release/ ) and unpack it, so that you have a - directory structure like so:: - - geonode-deb/ - + debian/ - + GeoNode-{version} - -* Run the debuild tool to build the package:: - - debuild -uc -us -A - -* geonode-{version}.deb will be produced in the parent directory (one level - above the directory where you cloned this project). - -Installation ------------- - -As described in the GeoNode manual, you can access OpenGeo's APT repository to -get pre-built GeoNode packages. However, if you want to build a package and -install that instead, you can avoid the need for a repository of your own by -using the following command:: - - dpkg -i geonode-{version}.deb - -If dpkg reports an error about unmet dependencies, you can issue the following -command to fetch dependencies and re-attempt the installation:: - - apt-get install -f - diff --git a/package/debian/changelog b/package/debian/changelog deleted file mode 100644 index f3f977dd5fe..00000000000 --- a/package/debian/changelog +++ /dev/null @@ -1,10568 +0,0 @@ -geonode (2.10.0+rc4) bionic; urgency=high - - * [dfa999fdb] 2018-07-31 - (afabiani ) - #3180 restoring angular 1.4.0 - * [573a6a9c3] 2018-07-31 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [0403b8f32] 2018-07-31 - (Alessio Fabiani ) Merge pull request #3866 from t-book/master - * [c509aa1d7] 2018-07-31 - (Toni Schönbuchner) allow long item titles to break without whitespace - * [5d53e7152] 2018-07-31 - (afabiani ) - #3180 restoring angular 1.4.0 - * [576cf8677] 2018-07-30 - (Alessio Fabiani ) Merge pull request #3864 from t-book/master - * [9c5035f5b] 2018-07-28 - (Toni Schönbuchner) prevent change of height on cart item hover - * [8734dfe8d] 2018-07-27 - (afabiani ) - Fix typo - * [2a17f205d] 2018-07-27 - (afabiani ) - Minor hardening on Map configuration stuff - * [ae30503c2] 2018-07-26 - (afabiani ) - Fix QGis integration tests - * [b5dac894d] 2018-07-26 - (afabiani ) - Fix QGis integration tests - * [bd18aac33] 2018-07-25 - (afabiani ) - Fix QGis Server Integration Tests - * [cf28e267d] 2018-07-26 - (Alessio Fabiani ) Merge pull request #3863 from t-book/master - * [32edbf515] 2018-07-25 - (Toni Schönbuchner) changed readme to rst with extension and changed setup.py - * [8fd79f941] 2018-07-25 - (Toni Schönbuchner) submission of new readme for github - * [fef4dfff2] 2018-07-25 - (afabiani ) - Fix QGis Server Integration Tests - * [7d8655708] 2018-07-25 - (Alessio Fabiani ) Merge pull request #3860 from geosolutions-it/master - * [64b223107] 2018-07-25 - (afabiani ) - Updating ElasticSearch dependencies - * [f36c050ed] 2018-07-25 - (Alessio Fabiani ) Merge branch 'master' into master - * [2c40bdbc4] 2018-07-25 - (Alessio Fabiani ) Merge pull request #3862 from boney-bun/master - * [1e854d4b6] 2018-07-25 - (Boney Bun ) fix a srid bug when uploading vector layers - * [0f43f323c] 2018-07-24 - (afabiani ) - Updating ElasticSearch dependencies - * [c1a49147f] 2018-07-24 - (afabiani ) - Remove circular local_settings import - * [92ffeafec] 2018-07-24 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [7feb3a8ff] 2018-07-24 - (Alessio Fabiani ) Merge pull request #3859 from t-book/master - * [2a1c59b4f] 2018-07-24 - (Toni Schönbuchner) centered magnifier in big search and aligned metadata checkbox - * [1bc9f4e47] 2018-07-24 - (afabiani ) - Tentative fix geoserver docker compose - * [d1de73b16] 2018-07-24 - (afabiani ) - Tentative fix geoserver docker compose - * [0ad5706bc] 2018-07-24 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [3708c3183] 2018-07-24 - (Alessio Fabiani ) Merge pull request #3856 from francbartoli/master - * [ef3e1acc0] 2018-07-24 - (Alessio Fabiani ) Merge branch 'master' into master - * [ac3cfa5cf] 2018-07-24 - (Alessio Fabiani ) Merge pull request #3858 from hishamkaram/patch-9 - * [2decdaae4] 2018-07-23 - (Hisham waleed k..) Update settings.py - * [c97413196] 2018-07-23 - (afabiani ) - Add storeType to Layers Capabilities response - * [1fa7f25cf] 2018-07-23 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [b958ce4a5] 2018-07-22 - (Francesco Bartoli) Merge remote-tracking branch 'origin/master' - * [3a4076281] 2018-07-22 - (Francesco Bartoli) Add variable to set geoserver JAVA_OPTS - * [02ae1b863] 2018-07-21 - (Alessio Fabiani ) Merge pull request #3852 from t-book/master - * [6dce23623] 2018-07-21 - (Alessio Fabiani ) Merge branch 'master' into master - * [9708bfed6] 2018-07-21 - (Alessio Fabiani ) Merge pull request #3854 from francbartoli/master - * [9353e37db] 2018-07-21 - (Francesco Bartoli) Fix #3853 - * [cd5addc4e] 2018-07-21 - (Alessio Fabiani ) - MapLoom GIS client hooksets (#3851) - * [8cd181006] 2018-07-21 - (Toni Schönbuchner) Added explanations regarding pygdal install #3784 - * [ac7a0292e] 2018-07-20 - (afabiani ) - MapLoom GIS client hooksets - * [821beb2b6] 2018-07-20 - (afabiani ) - MapLoom GIS client hooksets - * [b745808de] 2018-07-20 - (afabiani ) - Fix JS vulnerability - * [cf6965b36] 2018-07-20 - (afabiani ) - MapLoom GIS client hooksets - * [2af6c077f] 2018-07-20 - (afabiani ) - Fixing test-cases - * [97d7dd07a] 2018-07-19 - (afabiani ) Update gsconfig and gsimporter versions - * [1009131c1] 2018-07-19 - (afabiani ) - Remove ws prefixed URL from links in order to publish a full DescribeLayer on Links - * [ceda54816] 2018-07-19 - (afabiani ) - Update gnimporter version - * [a5ed83ce4] 2018-07-19 - (afabiani ) - Update gnimporter version - * [a944a4244] 2018-07-19 - (Alessio Fabiani ) Merge pull request #3845 from geosolutions-it/master - * [ebb162796] 2018-07-18 - (Alessio Fabiani ) Merge branch 'master' into master - * [7be86215a] 2018-07-16 - (Giovanni Allegri ) Merge pull request #3848 from giohappy/fix_docker_geonode_db - * [d71cc4ac1] 2018-07-16 - (giohappy ) fix GeoNode DB name in docker env - * [dff13c96c] 2018-07-15 - (Giovanni Allegri ) Merge pull request #3847 from giohappy/master - * [a7f5b0319] 2018-07-15 - (giohappy ) note on docker-compose up for Windows users (see #3709) - * [e80319885] 2018-07-13 - (afabiani ) - Minor Layout improvements - * [edbfa648c] 2018-07-13 - (afabiani ) - Minor Layout improvements - * [47af05841] 2018-07-13 - (afabiani ) - Exclude public-invite groups from metadata choices - * [77c1c24c3] 2018-07-13 - (Alessio Fabiani ) Merge branch 'master' into master - * [ed4ce3acc] 2018-07-13 - (afabiani ) [Fixes #3834] STATIC_URL vs static template tag - * [f54419382] 2018-07-13 - (Alessio Fabiani ) Merge pull request #3844 from geosolutions-it/master - * [f6bcae545] 2018-07-12 - (afabiani ) - Fixes issue #3843 - Fix vulnerability with Pillow dependency - * [0129b3364] 2018-07-11 - (Alessio Fabiani ) Merge pull request #3842 from t-book/master - * [9ca793131] 2018-07-11 - (Alessio Fabiani ) Merge branch 'master' into master - * [be78df272] 2018-07-11 - (Toni Schönbuchner) Restrict use of Edit Document Button - * [297697a72] 2018-07-10 - (Alessio Fabiani ) Merge pull request #3841 from t-book/master - * [9a10a8df3] 2018-07-10 - (Toni Schönbuchner) corrected Ubuntu 14.04 to 16.04 in documentation - * [78f1d477b] 2018-07-10 - (Toni Schönbuchner) added search input for styles to manage page - * [5761b91e3] 2018-07-07 - (afabiani ) - Fix max zoom issue - * [c5164506b] 2018-07-05 - (geo ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [3b23fca25] 2018-07-05 - (geo ) - Packagind scripts updates - * [7afb2fdb2] 2018-07-05 - (Alessio Fabiani ) Merge pull request #3839 from geosolutions-it/master - * [c44fe29fd] 2018-07-05 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [cc4bbe671] 2018-07-05 - (geo ) - Packagin scripts updates - * [706971057] 2018-07-05 - (afabiani ) - Improving French translations - * [d413250ef] 2018-07-04 - (afabiani ) - Allow portal contributors to invite users - * [55e234a84] 2018-07-04 - (Alessio Fabiani ) Merge pull request #3836 from geosolutions-it/master - * [866b7aaa6] 2018-07-04 - (Alessio Fabiani ) Merge branch 'master' into master - * [63518ec96] 2018-07-04 - (Alessio Fabiani ) Merge pull request #3838 from glennvorhes/add_ast_import - * [fc4008c39] 2018-07-03 - (Glenn Vorhes ) add missing ast import - * [a0279938b] 2018-07-03 - (afabiani ) - Fixes layer replace - * [0dfe7941e] 2018-07-02 - (afabiani ) - Fixes layer replace - * [de7078339] 2018-06-28 - (afabiani ) - Fix kombu/messaging initialization - * [85ec5112a] 2018-06-28 - (afabiani ) pep8 fixes - * [c67e5b8ff] 2018-06-28 - (afabiani ) - Fix celery initialization when using GeoNode ad a depenency - * [532bfbd33] 2018-06-28 - (afabiani ) - Fix celery initialization when using GeoNode ad a depenency - * [f6d61ff2a] 2018-06-28 - (afabiani ) - Fix celery initialization when using GeoNode ad a depenency - * [5a4db3d10] 2018-06-27 - (afabiani ) pep8 issues - * [a74a0ddc6] 2018-06-27 - (afabiani ) - Fix celery initialization when using GeoNode ad a depenency - * [5af259064] 2018-06-27 - (Alessio Fabiani ) Externalize OGC TIMEOUT setting as ENV var - * [49ce37c72] 2018-06-25 - (afabiani ) - Docker make use of GeoServer Importer Uploader - * [be9a7a401] 2018-06-25 - (afabiani ) - minor improvements geoserver helper - * [dd002d4a7] 2018-06-25 - (afabiani ) - Fix localhost docker compose var - * [5e289017e] 2018-06-25 - (afabiani ) - Tentative fix doscker-compose vars - * [22bbdbff8] 2018-06-22 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [87c41c473] 2018-06-22 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [35cec0b54] 2018-06-21 - (afabiani ) - Improve Map Embed Template and allow it passing through client hooksets - * [0756f0e9b] 2018-06-21 - (afabiani ) - Improve Map Embed Template and allow it passing through client hooksets - * [0df60c913] 2018-06-21 - (afabiani ) - Updating the oauth2 toolkit dep version - * [58f9d7b83] 2018-06-21 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [983d2e648] 2018-06-19 - (afabiani ) - GeoNode Client Hooksets: allow client configuration tweaking from pluggable client library - * [ebcddc869] 2018-06-17 - (Giovanni Allegri ) Merge pull request #3832 from GeoNode/datastore-var-patch - * [4da10180a] 2018-06-17 - (Francesco Bartoli) Don't raise an exception if variable is missing - * [9df16c60b] 2018-06-14 - (afabiani ) - Update requirements: adding openssl deps - * [f195a0c53] 2018-06-14 - (afabiani ) - Update requirements: adding openssl deps - * [65ab0d7a8] 2018-06-13 - (afabiani ) [Fixes #3800] Uploading shapefiles without a datefield and time-enabled is False in importer settings fails in 2.7.x - * [f4ff4c80e] 2018-06-12 - (afabiani ) - SITEURL rstrip (/) consistently - * [d6689fa5a] 2018-06-12 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [03e972ab5] 2018-06-11 - (afabiani ) Improvements to PyCSW Constraints and Local Mappings - * [ce79021bb] 2018-06-11 - (afabiani ) Improvements to PyCSW Constraints and Local Mappings - * [d4a875954] 2018-06-07 - (giohappy ) Set default datastore from env for OGC server settings - * [d1ffcaf00] 2018-06-08 - (Alessio Fabiani ) Merge pull request #3827 from GeoNode/ogc_datastore_from_env - * [e2dc1df67] 2018-06-07 - (afabiani ) Improvements to PyCSW Constraints and Local Mappings - * [e1b838608] 2018-06-07 - (giohappy ) value should be datastores key name, not value - * [56943548d] 2018-06-07 - (giohappy ) Set default datastore from env for OGC server settings - * [30a7c9b23] 2018-06-07 - (Alessio Fabiani ) Merge pull request #3825 from geosolutions-it/master - * [b2c2789a4] 2018-06-06 - (afabiani ) [Fw-port #3817] Implements GNIP #3718 (Worldmap contrib application) - * [abd6ac97c] 2018-06-06 - (afabiani ) [Fixes #3824] Manage style page show style name instead of title - * [117bbdccc] 2018-06-06 - (afabiani ) - OIDC 1.0 compliancy / notifications fixes - * [e7ddf9a56] 2018-06-04 - (afabiani ) - OIDC 1.0 compliancy preparation: add api > UserInfo method - * [73b949f1e] 2018-06-04 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [f5dde59ef] 2018-06-04 - (Alessio Fabiani ) Merge pull request #3822 from timlinux/master - * [096dc1029] 2018-06-02 - (Tim Sutton ) Fix issues where docker client may be incompatible with docker server API by forcing to APV version 1.24 - * [ed6f97690] 2018-05-29 - (afabiani ) - Add iso8961 time rules (yyyy/yyyy-mm/yyyy-mm-dd) on Templates also - * [990aa4b7f] 2018-05-28 - (Alessio Fabiani ) Merge pull request #3812 from geosolutions-it/master - * [5541329b0] 2018-05-28 - (Giovanni Allegri ) DATETIME_INPUT_FORMATS switched to list since Django 1.9 - * [06ef25456] 2018-05-28 - (afabiani ) [geoext client] - Zoom To Data and not to nearest Scale - * [ae4821318] 2018-05-24 - (Alessio Fabiani ) Merge branch 'master' into master - * [be6f92bd8] 2018-05-24 - (afabiani ) - pep8 issues - * [b21a0b887] 2018-05-24 - (afabiani ) - Update pip install instructions on docs and README - * [914ccb200] 2018-05-24 - (afabiani ) - Include django-celery-mon dep on requirements.txt - * [5b854c5ff] 2018-05-24 - (afabiani ) - ASYNC MODE uses ASYNC CELERY TASKS - * [e4774bf26] 2018-05-23 - (afabiani ) - ImageMosaics refactoring: first step - support ZIP archives with granules and .properties files - * [4a346bd57] 2018-05-23 - (afabiani ) - Update dependencies versions - * [2a16bf00d] 2018-05-23 - (afabiani ) - Fix Map Detail page structure issue and errors with GetCapabilities - * [df12893ce] 2018-05-22 - (Alessio Fabiani ) Merge pull request #3807 from geosolutions-it/master - * [6c0e8ca5e] 2018-05-21 - (afabiani ) - Restored the possibility of sending multiple uploads - * [15123a540] 2018-05-21 - (afabiani ) - Translations and minor refactoring of upload validator - * [fc9f14c91] 2018-05-16 - (afabiani ) - Reduced size of layer-upload tooltips square - * [bdf662488] 2018-05-16 - (afabiani ) - Allow registered users to manage Remote Services - * [c46fcc5a0] 2018-05-16 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [ddeebf561] 2018-05-16 - (afabiani ) - Correct management of SLDs / Add GWC filterParameter to SLDs - * [d87b19040] 2018-05-16 - (Alessio Fabiani ) Merge branch 'master' into master - * [7b32bb1dc] 2018-05-15 - (afabiani ) - Correct management of SLDs / Add GWC filterParameter to SLDs - * [df941093f] 2018-05-15 - (Alessio Fabiani ) Merge pull request #3805 from geosolutions-it/master - * [57f8e6f74] 2018-05-15 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [4c10a8561] 2018-05-15 - (afabiani ) - Increase Test Coverage - * [c358f24bf] 2018-05-15 - (afabiani ) Merge - * [f44471237] 2018-05-15 - (afabiani ) - Allow unapproved layers to be published on maps - * [07ebfeee3] 2018-05-15 - (afabiani ) - Fix for issue Map Composer Menu not show complete #3804 - * [dbde9664d] 2018-05-14 - (afabiani ) - Test GeoServer Integration Tests running with Docker Compose - * [cf3247af8] 2018-05-14 - (afabiani ) - Test GeoServer Integration Tests running with Docker Compose - * [83976d8b7] 2018-05-14 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [325b27246] 2018-05-14 - (afabiani ) - Fix ResponseNotReady issue - * [d32f11f18] 2018-05-14 - (afabiani ) - Test GeoServer Integration Tests running with Docker Compose - * [bbaef20b2] 2018-05-14 - (afabiani ) - Test GeoServer Integration Tests running with Docker Compose - * [02fbe2043] 2018-05-14 - (Alessio Fabiani ) Merge pull request #3802 from ADB-SPADE/master - * [c9e396634] 2018-05-14 - (Alessio Fabiani ) Merge branch 'master' into master - * [6c3b2b997] 2018-05-14 - (afabiani ) - Fix celery tasks hanging forever - * [959219b06] 2018-05-14 - (afabiani ) - Minor improvements - * [872e36631] 2018-05-14 - (Alessio Fabiani ) Merge branch 'master' into master - * [d6f784faf] 2018-05-11 - (erwin ) Generalize logo-urls in profile-detail template. - * [b0bb69926] 2018-05-11 - (Alessio Fabiani ) Merge pull request #3798 from geosolutions-it/docker-compose - * [5ec88bc1c] 2018-05-11 - (afabiani ) - Docker Compose improvs - * [38177424d] 2018-05-10 - (afabiani ) - minor tweak on settings for Docker - * [ac173fcc0] 2018-05-09 - (afabiani ) - Dockerfile: update pip install - * [f74ab0731] 2018-05-09 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [a01de4b0a] 2018-05-09 - (Alessio Fabiani ) Merge pull request #3795 from geosolutions-it/monitoring_geoip2 - * [0b0c2b794] 2018-05-09 - (afabiani ) - Travis pip cache - * [695695082] 2018-05-09 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode into monitoring_geoip2 - * [2e548df4c] 2018-05-09 - (afabiani ) - Test coverage - * [a8e67f6ab] 2018-05-09 - (afabiani ) - Integration test coverage - * [edf804405] 2018-05-09 - (Alessio Fabiani ) Merge branch 'master' into monitoring_geoip2 - * [35af12476] 2018-05-09 - (Alessio Fabiani ) Merge pull request #3796 from geosolutions-it/twisted_requirements - * [7e9c14d8e] 2018-05-09 - (afabiani ) - Integration test coverage - * [eb5de195a] 2018-05-09 - (afabiani ) - pep8 issues - * [659b23ada] 2018-05-09 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [8d8cb5453] 2018-05-09 - (afabiani ) - Minor fixes to backup & restore commands - * [7ca44cd19] 2018-05-09 - (Alessio Fabiani ) Merge branch 'master' into monitoring_geoip2 - * [463e971d9] 2018-05-09 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [0ac411c2a] 2018-05-08 - (Cezary Statkiew..) Merge remote-tracking branch 'geonode/master' into monitoring_geoip2 - * [8e87da4d0] 2018-05-08 - (Cezary Statkiew..) test-specific requirements: twisted - * [05e2a1b4e] 2018-05-08 - (afabiani ) - Legend links for remote services - * [669b816ec] 2018-05-08 - (afabiani ) - Legend links for remote services - * [f0366b2e4] 2018-05-08 - (Cezary Statkiew..) monitoring: resolve 2-letter codes to 3-letter codes - * [0dcca888b] 2018-05-08 - (Cezary Statkiew..) monitoring support for geoip2 - * [360f7ddfd] 2018-05-08 - (afabiani ) - Fix updatelayers mgmt command - * [4e1563ac6] 2018-05-08 - (Cezary Statkiew..) monitoring support for geoip2 - * [799f049aa] 2018-05-08 - (Cezary Statkiew..) monitoring support for geoip2 - * [501edfac6] 2018-05-08 - (afabiani ) - Monitoring GeoIP error management - * [92f8e9235] 2018-05-08 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [d9d8e84ef] 2018-05-08 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [6382e3bab] 2018-05-08 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [d4ebe63ef] 2018-05-08 - (Cezary Statkiew..) Monitoring geoip2 (#286) - * [6044d0a39] 2018-05-08 - (Alessio Fabiani ) Merge pull request #3794 from geosolutions-it/monitoring_geoip2 - * [f6658e1c6] 2018-05-08 - (afabiani ) - Disabling synchronous remote services probe from model - * [98b752904] 2018-05-08 - (Cezary Statkiew..) handle new geoip format properly - * [9dba9c2fa] 2018-05-08 - (Cezary Statkiew..) Merge remote-tracking branch 'geonode/master' into monitoring_geoip2 - * [38db65e3a] 2018-05-08 - (Cezary Statkiew..) use maxmind v2 db format if needed - * [25d4f3f1f] 2018-05-07 - (afabiani ) - Improve Test Coverage - * [b97f23b5b] 2018-05-07 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [d61720936] 2018-05-07 - (Alessio Fabiani ) Merge pull request #3780 from GeoNode/ISSUE_3662 - * [44ec68869] 2018-05-07 - (Alessio Fabiani ) Update helpers.py - * [27efde124] 2018-05-07 - (afabiani ) - Test coverage improvements - * [bc5992a32] 2018-05-07 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [d762796e0] 2018-05-04 - (giohappy ) included default settings for social providers - * [2e4bd5ad7] 2018-04-29 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3662 - * [2a0d06f5a] 2018-04-29 - (Alessio Fabiani ) Merge pull request #3750 from cartologic/add-geometry-type-to-layer - * [7b37cb1bb] 2018-04-29 - (Alessio Fabiani ) Merge branch 'master' into add-geometry-type-to-layer - * [835068825] 2018-04-28 - (Alessio Fabiani ) Update helpers.py - * [8652c27cc] 2018-04-26 - (Alessio Fabiani ) Merge with master - * [6af5f5155] 2018-04-26 - (afabiani ) - fixes and improvements to Layer replase functionalities - * [06166fe5f] 2018-04-26 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [c33701ef8] 2018-04-26 - (Alessio Fabiani ) Merge pull request #3781 from GeoNode/MINOR_IMPROVS_TMP - * [172c79cb0] 2018-04-26 - (Alessio Fabiani ) Merge pull request #3782 from geosolutions-it/messaging_error_handling - * [7881606f5] 2018-04-26 - (Ahmed Nour Eldeen) Merge branch 'master' into add-geometry-type-to-layer - * [fe14d724a] 2018-04-26 - (Ahmed Nour Eldeen) check geometry type - * [401ad5b13] 2018-04-26 - (afabiani ) - Fix layer replase - * [c47b75a94] 2018-04-26 - (afabiani ) - Fix remote services layout - * [3b94ba24b] 2018-04-26 - (Cezary Statkiew..) catch geoserver error in messaging, to avoid looped delivery - * [c085b83c0] 2018-04-26 - (Alessio Fabiani ) Update utils.py - * [ae62bfd86] 2018-04-26 - (afabiani ) - Fix test cases - * [5753b664c] 2018-04-26 - (afabiani ) - Fix test cases - * [db24fe3e1] 2018-04-26 - (Alessio Fabiani ) - Fix test cases - * [c01462323] 2018-04-26 - (afabiani ) - Fix test cases - * [6c1d60eab] 2018-04-26 - (Alessio Fabiani ) Updated changelog for version 2.8 - * [95e91b7b3] 2018-04-26 - (Alessio Fabiani ) Constrain pip to 9.0.3 - * [419d2e1fa] 2018-04-26 - (afabiani ) - Restore production/docker requirements - * [65bcf7461] 2018-04-26 - (afabiani ) - DB consistency checks - * [07271fa5b] 2018-04-26 - (Alessio Fabiani ) Update utils.py - * [b996b4b63] 2018-04-26 - (Alessio Fabiani ) Update utils.py - * [32ddf3c33] 2018-04-26 - (afabiani ) - Update avatar version - * [7d8d53c58] 2018-04-26 - (Alessio Fabiani ) Update utils.py - * [4a66e0891] 2018-04-26 - (afabiani ) Merge branch 'MINOR_IMPROVS_TMP' of https://github.com/GeoNode/geonode - * [991ac09e5] 2018-04-26 - (afabiani ) Merge branch 'ISSUE_3662' of https://github.com/GeoNode/geonode - * [ba3fb4b54] 2018-04-26 - (Alessio Fabiani ) Update utils.py - * [e25f154d3] 2018-04-24 - (afabiani ) Just fix requirements versions for GeoNode modules in order to avoid compatibility issues - * [2e77def04] 2018-04-24 - (afabiani ) Minor improvement to custom_theme_html template - * [4e14df5a8] 2018-04-24 - (afabiani ) [Closes #3662] GNIP: Improvements to GeoNode Layers download links - * [9af820274] 2018-04-24 - (afabiani ) Just fix requirements versions for GeoNode modules in order to avoid compatibility issues - * [1dd9e0264] 2018-04-24 - (afabiani ) Minor improvement to custom_theme_html template - * [8d6f5379f] 2018-04-24 - (afabiani ) [Closes #3662] GNIP: Improvements to GeoNode Layers download links - * [77f046fce] 2018-04-24 - (Alessio Fabiani ) Merge pull request #3779 from geosolutions-it/monitoring_geoip2 - * [602e1a06b] 2018-04-24 - (Cezary Statkiew..) use geoip2 for monitoring - * [8a7818cfb] 2018-04-24 - (Alessio Fabiani ) Merge pull request #3776 from PacificCommunity/fix-slow-login - * [def7df173] 2018-04-24 - (olivierdalang ) fix slow login/logout on certain circumstances - * [dee03ba92] 2018-04-23 - (afabiani ) - Fix reproj issue on bbox_to_projection - * [4bce2e53b] 2018-04-23 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [50705bba9] 2018-04-23 - (Alessio Fabiani ) Merge pull request #3757 from GeoNode/ISSUE_3661 - * [36467bc57] 2018-04-23 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3661 - * [68d28f119] 2018-04-23 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [7f6e0ee63] 2018-04-23 - (Alessio Fabiani ) Merge pull request #3770 from GeoNode/ISSUE_3769 - * [cef3301cf] 2018-04-23 - (afabiani ) Merge with master - * [f8864cf75] 2018-04-23 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [3817491d9] 2018-04-23 - (afabiani ) Merge with master - * [0aa2f644d] 2018-04-23 - (Alessio Fabiani ) - Support for pip >= 10 - * [e379122b3] 2018-04-23 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3769 - * [531bd0bc2] 2018-04-23 - (Alessio Fabiani ) Merge pull request #3766 from PacificCommunity/reuse_requirements_in_setup - * [86fed383c] 2018-04-23 - (Alessio Fabiani ) Merge pull request #3773 from PacificCommunity/fix-3643 - * [205823abb] 2018-04-23 - (Olivier ) [FIX #3643] better display for discover section on homepage - * [634630164] 2018-04-23 - (Olivier ) missing dep for integrations test - * [152b33631] 2018-04-18 - (Olivier ) use same deps for requirements.txt and setup.txt - * [db57dd3c7] 2018-04-21 - (Alessio Fabiani ) Update settings.py - * [f50b2402e] 2018-04-21 - (Alessio Fabiani ) Merge branch 'ISSUE_3769' of https://github.com/GeoNode/geonode into ISSUE_3769 - * [ef7666172] 2018-04-21 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3769 - * [ca856e148] 2018-04-21 - (afabiani ) Merge branch 'ISSUE_3661' of https://github.com/GeoNode/geonode into ISSUE_3661 - * [d1aff2871] 2018-04-21 - (afabiani ) Merge with master - * [8d80a6f46] 2018-04-21 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3661 - * [ceee3d0a4] 2018-04-21 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3661 - * [4fbbaf63a] 2018-04-21 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3769 - * [6a8984734] 2018-04-21 - (Alessio Fabiani ) Merge pull request #3772 from GeoNode/IMPORTER_TIME_DIM_FIXES - * [efd2441a0] 2018-04-21 - (Alessio Fabiani ) Merge pull request #3767 from GeoNode/remove-rtd-patch - * [1a4453e6c] 2018-04-21 - (Alessio Fabiani ) - Fix settings test cases - * [7d3350ab1] 2018-04-21 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3769 - * [901054654] 2018-04-20 - (Alessio Fabiani ) - Fix Importer Time Dimensions Transform and KMZ vx KML overlays - * [4f244436d] 2018-04-20 - (Alessio Fabiani ) Merge branch 'master' into remove-rtd-patch - * [0d49c5a55] 2018-04-20 - (Alessio Fabiani ) Merge pull request #3771 from geosolutions-it/monitoring_gs_check - * [16c9fc96d] 2018-04-20 - (Alessio Fabiani ) - Fix bdd tests - * [7e5ebbf11] 2018-04-20 - (Cezary Statkiew..) do not check url from gs in monitoring - * [d6622b147] 2018-04-20 - (Alessio Fabiani ) [Fixes #3769] - Pip install geonode complains about Celery - * [52d5650e6] 2018-04-20 - (Alessio Fabiani ) - Reload uploaded layer from catalog - * [95391f8c4] 2018-04-20 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3661 - * [1a03e7279] 2018-04-20 - (Alessio Fabiani ) Update setup.py - * [5adacc647] 2018-04-20 - (Alessio Fabiani ) Merge pull request #3762 from GeoNode/ISSUE_3710 - * [928465e37] 2018-04-20 - (Alessio Fabiani ) Merge pull request #3764 from geosolutions-it/monitoring_autoconfigure_service_last_check - * [64778d23a] 2018-04-20 - (Cezary Statkiew..) service.last_check default value in model - * [4427f9b19] 2018-04-20 - (Cezary Statkiew..) Merge remote-tracking branch 'geonode/master' into monitoring_autoconfigure_service_last_check - * [89b75e754] 2018-04-20 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3661 - * [e7a45e619] 2018-04-20 - (afabiani ) - Restoring Live Server port settings on integration tests in order to avoid address conflicts - * [8de19bd43] 2018-04-20 - (Giovanni Allegri ) remove fix for readthedocs - * [07e40e5a4] 2018-04-20 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3710 - * [c3a2405ac] 2018-04-20 - (Alessio Fabiani ) Merge pull request #3765 from PacificCommunity/pin-autocomplete-light-dep - * [ed8f1a704] 2018-04-20 - (Olivier ) pin lxml to 3.6.2 - * [04a5bfa40] 2018-04-20 - (Olivier ) pin django-autocomplete-light to 2.3.3 - * [67972eaf5] 2018-04-19 - (Cezary Statkiew..) populate service.last_check in monitoring in autoconfigure - * [8b93fa24b] 2018-04-19 - (afabiani ) - Split test cases on travis - * [cf61164bd] 2018-04-19 - (afabiani ) - Split test cases on travis - * [4be6c0d65] 2018-04-19 - (Alessio Fabiani ) - Fixes: allow raster zip files also - * [36452db1b] 2018-04-19 - (Alessio Fabiani ) - Fixes KML upload issue - * [f2bb89318] 2018-04-19 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3661 - * [e7a1dd75b] 2018-04-19 - (Alessio Fabiani ) - Fix test cases and minor improvements to monitoring plugin - * [3d0e064c2] 2018-04-19 - (Alessio Fabiani ) - Re-enable LOGGER handlers on settings - * [b4935651d] 2018-04-19 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode into ISSUE_3661 - * [91d284ef2] 2018-04-19 - (Alessio Fabiani ) - Minor improvements to remote services - * [cab688e77] 2018-04-19 - (afabiani ) - Align with master - * [0db74dea3] 2018-04-19 - (Alessio Fabiani ) - Minor fixes and cleanup - * [23c25cede] 2018-04-19 - (afabiani ) Merge branch 'ISSUE_3661' of https://github.com/GeoNode/geonode into ISSUE_3661 - * [68c833e56] 2018-04-19 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3661 - * [39a404b48] 2018-04-19 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [c0c290bb4] 2018-04-19 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3661 - * [e92165d2c] 2018-04-18 - (afabiani ) - cleanup - * [c7edb2234] 2018-04-18 - (afabiani ) - align with master branch - * [74e4b46cf] 2018-04-18 - (Alessio Fabiani ) Merge pull request #3760 from t-book/master - * [b59d00fd5] 2018-04-18 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode into ISSUE_3661 - * [2bf2a48f6] 2018-04-18 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3661 - * [9af3354ea] 2018-04-18 - (Alessio Fabiani ) - Typo in Remote Services Handlers - * [f73b843d6] 2018-04-18 - (Alessio Fabiani ) - Fixes for GeoServer Importer Uploader - * [e0cdcfb2a] 2018-04-18 - (Toni Schönbuchner) aligned lines in code block - * [3e39bfd9a] 2018-04-18 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [9b680c559] 2018-04-18 - (Alessio Fabiani ) Merge pull request #3759 from GeoNode/PR_3758 - * [e37f2d454] 2018-04-18 - (Alessio Fabiani ) - Disabling synchronous Remote Services probe - * [b4558f944] 2018-04-18 - (Alessio Fabiani ) [Rebased] Changed pattern for user name in capabilities url (added +@) - * [61e776fe0] 2018-04-18 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode into geosolutions_master - * [26345e2e7] 2018-04-18 - (Alessio Fabiani ) - Improvements and fixes to Remote Services - * [2519f3fc0] 2018-04-18 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3661 - * [67206f799] 2018-04-17 - (Alessio Fabiani ) Better alignment of jumbotron image - * [c20fd2166] 2018-04-17 - (afabiani ) - Fix integration test cases - * [d05ba3f8a] 2018-04-17 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3661 - * [c0b298f69] 2018-04-16 - (afabiani ) [Closes #3661] django 1.11 LTS support on master - * [d66e89da7] 2018-04-17 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [ef8fcd21b] 2018-04-17 - (Alessio Fabiani ) Better alignment of jumbotron image - * [5972d18a8] 2018-04-16 - (afabiani ) [Closes #3661] django 1.11 LTS support on master - * [b17b40a19] 2018-04-16 - (afabiani ) [Fixes #3753] GeoExt Maps: thumbnail is not correctly saved the first time I save the map / [Fixes #3752] GeoExt Maps do not preserve the saved zoom level - * [dba64dc22] 2018-04-16 - (afabiani ) Fix GeoNode test framework timeouts on Travis - * [a12447fb7] 2018-04-16 - (afabiani ) [Closes] Top margin for jumbotron backgroung image / Option to hide message overlaid on jumbotron - * [4a4b865f9] 2018-04-16 - (afabiani ) Fix GeoNode test framework timeouts on Travis - * [c222d06be] 2018-04-16 - (afabiani ) Fix GeoNode test framework timeouts on Travis - * [32873ce39] 2018-04-16 - (afabiani ) Fix GeoNode test framework timeouts on Travis - * [95154907c] 2018-04-16 - (afabiani ) Fix GeoNode test framework timeouts on Travis - * [848f0eb88] 2018-04-16 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [cb46b66bf] 2018-04-16 - (Alessio Fabiani ) Merge pull request #3756 from t-book/master - * [5b798f08f] 2018-04-15 - (Toni Schönbuchner) #3754: corrected geonode Version. Fixed tomcat reference - * [0a3c05e1a] 2018-04-14 - (afabiani ) Trying to speed up tests - * [413e8d836] 2018-04-14 - (afabiani ) Fix pinax notifications namespace - * [af0fedffb] 2018-04-14 - (afabiani ) Faster test cases - * [bf16f43e1] 2018-04-14 - (afabiani ) Fix UI html template - * [f767b281d] 2018-04-13 - (afabiani ) Fix ui customization template: email field - * [942abb826] 2018-04-13 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [ee8eb2883] 2018-04-13 - (afabiani ) Trying to revert to sequential nose test suite (until Django upgrade) - * [3897847fd] 2018-04-13 - (afabiani ) GeoNode Test Suite: minor improvements - * [9f34d4919] 2018-04-13 - (afabiani ) Monitoring: pep8 fixes - * [7d31e6ff1] 2018-04-13 - (afabiani ) [Monitoring] collect metrics api url exposed - * [1987172b3] 2018-04-13 - (Alessio Fabiani ) Merge pull request #3725 from GeoNode/ISSUE_3711 - * [097829014] 2018-04-13 - (afabiani ) Monitoring auto_configure hosts - * [42ea0d74c] 2018-04-13 - (afabiani ) GeoNode test case refactoring - * [baff1ab5b] 2018-04-13 - (afabiani ) GeoNode test case refactoring - * [9d545611c] 2018-04-13 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [e09df84b1] 2018-04-13 - (afabiani ) GeoNode test case refactoring - * [7b5f71ad4] 2018-04-12 - (afabiani ) [Closes #3743] [GNIP] GeoNode UI customization from admin - add partners management - * [2b68db2df] 2018-04-13 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [410f0b0e5] 2018-04-13 - (Alessio Fabiani ) Merge pull request #3744 from GeoNode/ISSUE_3743 - * [a0dc1aeaa] 2018-04-13 - (Alessio Fabiani ) Merge pull request #3751 from ADB-SPADE/master - * [6e84a81ad] 2018-04-13 - (Alessio Fabiani ) Merge pull request #3729 from GeoNode/ISSUE_3719 - * [006ea7294] 2018-04-13 - (Alessio Fabiani ) Merge pull request #3724 from GeoNode/ISSUE_3705 - * [8afe731e2] 2018-04-12 - (afabiani ) GeoNode test case refactoring - * [d3bb449a2] 2018-04-12 - (Unknown ) Add .shp file to file_paths for validate_shapefile_components - * [9a841476b] 2018-04-12 - (afabiani ) [Closes #3743] [GNIP] GeoNode UI customization from admin - * [7c9de6bb2] 2018-04-12 - (afabiani ) Merge branch 'master' of https://github.com/geosolutions-it/geonode - * [f5c026cc9] 2018-04-12 - (afabiani ) [Closes #3743] [GNIP] GeoNode UI customization from admin - * [9e7a9e89e] 2018-04-12 - (afabiani ) Merge - * [7120a8ee6] 2018-04-11 - (afabiani ) GeoNode test case refactoring - * [ad798d0b2] 2018-04-11 - (afabiani ) [Fixes #3747] Gxp time-slider not showing up after recent bbox adjustments - * [7fbe00405] 2018-04-11 - (giohappy ) Reference GeoExplorer's github repo in docs - * [9c87dcb2a] 2018-04-11 - (giohappy ) Reference GeoExplorer's github repo in docs - * [10e97f399] 2018-04-11 - (afabiani ) GeoNode test case refactoring - * [bb9696b9c] 2018-04-11 - (afabiani ) GeoNode test case refactoring - * [62d7ad950] 2018-04-11 - (afabiani ) GeoNode test case refactoring - * [aad557f92] 2018-04-11 - (Ahmed Nour Eldeen) get geometry type for layers - * [9a91decf6] 2018-04-11 - (Alessio Fabiani ) fix READTHEDOCS - * [34db45a71] 2018-04-11 - (afabiani ) fix READTHEDOCS - * [935fa2dea] 2018-04-11 - (afabiani ) fix READTHEDOCS - * [b8f818952] 2018-04-11 - (Alessio Fabiani ) Delete .readthedocs.yml - * [6d174f38f] 2018-04-11 - (afabiani ) fix READTHEDOCS - * [53e79e30b] 2018-04-11 - (afabiani ) fix READTHEDOCS - * [c7aa4cbff] 2018-04-11 - (afabiani ) fix READTHEDOCS - * [15c41fa0b] 2018-04-11 - (afabiani ) [Fixes #3747] Gxp time-slider not showing up after recent bbox adjustments - * [79b92e74c] 2018-04-11 - (afabiani ) [Fixes #3745] Adding linked document deletes all other linked documents - * [024caa25e] 2018-04-11 - (afabiani ) fix READTHEDOCS - * [68c5cd426] 2018-04-11 - (Alessio Fabiani ) Fix READTHEDOCS - * [63cd88ca7] 2018-04-11 - (giohappy ) Reference GeoExplorer's github repo in docs - * [6d9cacd57] 2018-04-11 - (afabiani ) [Fixes #3747] Gxp time-slider not showing up after recent bbox adjustments - * [46c37163c] 2018-04-11 - (afabiani ) [Fixes #3745] Adding linked document deletes all other linked documents - * [58e923377] 2018-04-10 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3719 - * [804e140c8] 2018-04-09 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into ISSUE_3711 - * [e6e1aa7a0] 2018-04-09 - (afabiani ) - Refreshing static files compiled with updated versions of bower and grunt - * [f46c398ef] 2018-04-09 - (afabiani ) [Closes #3743] [GNIP] GeoNode UI customization from admin - * [5589619ca] 2018-04-09 - (afabiani ) - Refreshing static files compiled with updated versions of bower and grunt - * [53c68a9c0] 2018-04-09 - (Alessio Fabiani ) Update setup.py - * [b4e00d119] 2018-04-09 - (afabiani ) [Closes #3743] [GNIP] GeoNode UI customization from admin - * [fcbf7d36f] 2018-04-09 - (afabiani ) Merge with master - * [d35c19718] 2018-04-09 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3705 - * [093e72ed5] 2018-04-09 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3719 - * [46ae4acda] 2018-04-09 - (Alessio Fabiani ) Merge pull request #3741 from t-book/master - * [8f4e4462c] 2018-04-09 - (Alessio Fabiani ) Merge pull request #3734 from GeoNode/ISSUE_3703 - * [c83e4cee1] 2018-04-09 - (Toni Schönbuchner) Updated vagrant tutorial for Ubuntu 16.04 - * [d2def7e10] 2018-04-09 - (Alessio Fabiani ) Update requirements.txt - * [24c077c29] 2018-04-09 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [47b1219b0] 2018-04-09 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3719 - * [47d4a5d62] 2018-04-09 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3705 - * [638deda3a] 2018-04-09 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3703 - * [1ea789cbe] 2018-04-09 - (Alessio Fabiani ) Merge pull request #3728 from GeoNode/ISSUE_3715 - * [e55f5be78] 2018-04-08 - (afabiani ) Merge - * [0a2528589] 2018-04-08 - (Alessio Fabiani ) Merge pull request #3666 from cartologic/update_layers - * [29fb0cd83] 2018-04-08 - (Alessio Fabiani ) Update setup.py - * [41b474d27] 2018-04-08 - (Alessio Fabiani ) Update setup.py - * [7273c78d8] 2018-04-08 - (Alessio Fabiani ) Update requirements.txt - * [43c2e6c84] 2018-04-08 - (Alessio Fabiani ) Fix pep8 - * [007d12ed0] 2018-04-08 - (Alessio Fabiani ) Merge branch 'master' into update_layers - * [ed0e94723] 2018-04-08 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3705 - * [6338e39fb] 2018-04-08 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3715 - * [6fe5c05e0] 2018-04-08 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3719 - * [a99d1d760] 2018-04-08 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3703 - * [045c0366e] 2018-04-08 - (Alessio Fabiani ) Merge pull request #3730 from GeoNode/ISSUE_3720 - * [cc2938369] 2018-04-08 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3719 - * [d841f0a43] 2018-04-08 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3715 - * [a8c4887e4] 2018-04-07 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3705 - * [d070707af] 2018-04-07 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3720 - * [a4618c33d] 2018-04-07 - (Alessio Fabiani ) Merge pull request #3726 from GeoNode/ISSUE_3712 - * [a25c10108] 2018-04-07 - (Alessio Fabiani ) Merge pull request #3733 from GeoNode/ISSUE_3702 - * [2a3b7e7e4] 2018-04-07 - (Alessio Fabiani ) Merge branch 'master' into ISSUE_3712 - * [170b63dc2] 2018-04-07 - (Alessio Fabiani ) Merge pull request #3737 from geosolutions-it/allowed_hosts_parsing - * [54e678509] 2018-04-07 - (Alessio Fabiani ) Merge branch 'master' into allowed_hosts_parsing - * [c0313590b] 2018-04-07 - (Alessio Fabiani ) Merge pull request #3736 from geosolutions-it/requirements_no_sqlalchemy - * [182ffabcc] 2018-04-07 - (Alessio Fabiani ) Merge branch 'master' into requirements_no_sqlalchemy - * [c5bfc9181] 2018-04-07 - (Alessio Fabiani ) Merge pull request #3731 from GeoNode/ISSUE_3721 - * [81ff840c6] 2018-04-07 - (Alessio Fabiani ) Merge pull request #3732 from GeoNode/ISSUE_3700 - * [58e5e62c1] 2018-04-07 - (Alessio Fabiani ) Merge branch 'master' into requirements_no_sqlalchemy - * [ff44ceffb] 2018-04-06 - (Paolo Corti ) Merge branch 'master' into ISSUE_3721 - * [12bbb1ad9] 2018-04-06 - (Paolo Corti ) Merge branch 'master' into ISSUE_3700 - * [8fa626967] 2018-04-06 - (Alessio Fabiani ) [Fixes #3704] Protect and hide views and pages accessible to superuser only (#3735) - * [95aff5c9b] 2018-04-06 - (Cezary Statkiew..) support for different notations of allowed_hosts env var - * [63b10610a] 2018-04-06 - (Cezary Statkiew..) removed unused sqlalchemy - * [32d04ac13] 2018-04-06 - (Paolo Corti ) Merge branch 'master' into ISSUE_3700 - * [c9dfd2803] 2018-04-06 - (Alessio Fabiani ) [Closes #3713] Update settings password hashers (#3727) - * [dd52d8324] 2018-04-03 - (afabiani ) [Fixes #3703] Upgrade pycsw to the new version 2.2.0 - * [6d96f9aa9] 2018-04-03 - (Alessio Fabiani ) [Fixes #3702] Improve settings and documentation accordingly for CELERY and MAP DEFAULT CRS - * [9b06f066b] 2018-04-03 - (afabiani ) [Fixes #3700] Map detail page wrong zoom - * [0ec01c4c1] 2018-04-06 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [052a6d0d6] 2018-04-05 - (afabiani ) [Closes #3721] Align contrib apps with Django improvements and remove print statements - * [d3f68ae7e] 2018-04-05 - (afabiani ) [Closes #3720] Improve Base Regions Form Widget: remove deprecated stuff and use region IDs consistently - * [adeaeff19] 2018-04-05 - (afabiani ) [Fixes #3719] Complete amqp configuration for ASYNC messaging system - * [fecf8adf8] 2018-04-05 - (afabiani ) [Fixes #3719] Complete amqp configuration for ASYNC messaging system - * [45a9d8fa4] 2018-04-05 - (afabiani ) [Closes #3715] Remove deprecated BaseCommand.option_list from management commands - * [1ba384666] 2018-04-04 - (afabiani ) [Fixes #3712] Fix Layer getFeatureInfo Property Labels - * [b687c0e0d] 2018-04-04 - (afabiani ) [Closes #3711] Update GeoServer to 2.13 version - * [6b6ebddcb] 2018-04-04 - (afabiani ) [Fixes #3705] Add GeoNode (OWS) and ArcGIS Endpoints to Remote Services - * [07c0a9590] 2018-04-06 - (Alessio Fabiani ) Merge pull request #3723 from lucernae/fix_qgis_server - * [76c0403a0] 2018-04-03 - (Rizky Maulana N..) Fix QGIS Server settings sample - * [27c00e402] 2018-04-05 - (afabiani ) [Fixes #3719] Complete amqp configuration for ASYNC messaging system - * [4a93ee546] 2018-04-05 - (afabiani ) [Closes #3721] Align contrib apps with Django improvements and remove print statements - * [19efe55a6] 2018-04-05 - (afabiani ) [Closes #3720] Improve Base Regions Form Widget: remove deprecated stuff and use region IDs consistently - * [def176524] 2018-04-05 - (afabiani ) [Fixes #3719] Complete amqp configuration for ASYNC messaging system - * [4e91c60dc] 2018-04-05 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [b48e34731] 2018-04-05 - (Alessio Fabiani ) Merge pull request #3717 from sharon-tickell/geoserver-datastore-fix - * [5510dba6c] 2018-04-05 - (tic016 ) Use the correct datastore name in _create_db_featurestore The datastore-name is not necessarily the same as the datastore-database-name, and other upload/create datastore functions all use ogc_server_settings.DATASTORE - * [08c09552f] 2018-04-05 - (afabiani ) [Closes #3715] Remove deprecated BaseCommand.option_list from management commands - * [98d496055] 2018-04-04 - (afabiani ) [Closes #3713] Update settings password hashers - * [f557aec98] 2018-04-04 - (afabiani ) [Fixes #3712] Fix Layer getFeatureInfo Property Labels - * [753e98cac] 2018-04-04 - (afabiani ) [Closes #3711] Update GeoServer to 2.13 version - * [864db65bd] 2018-04-04 - (afabiani ) [Fixes #3705] Add GeoNode (OWS) and ArcGIS Endpoints to Remote Services - * [eb7dace92] 2018-04-04 - (afabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode - * [f67a81d87] 2018-04-04 - (Alessio Fabiani ) Merge pull request #3706 from ADB-SPADE/master - * [afca6e2ee] 2018-04-03 - (Unknown ) Documentation on Google & Bing basemaps - * [34f0f8535] 2018-04-03 - (afabiani ) [Fixes #3704] Protect and hide views and pages accessible to superuser only - * [741c871d9] 2018-04-03 - (afabiani ) [Fixes #3703] Upgrade pycsw to the new version 2.2.0 - * [ba0e2f845] 2018-04-03 - (Alessio Fabiani ) [Fixes #3702] Improve settings and documentation accordingly for CELERY and MAP DEFAULT CRS - * [6e112d5a9] 2018-04-03 - (afabiani ) [Fixes #3700] Map detail page wrong zoom - * [683adc0f7] 2018-04-03 - (Alessio Fabiani ) Update README - * [1169fd3e6] 2018-04-03 - (afabiani ) Forward port 2.8.0 stable fixes - * [de4eaae8a] 2018-04-03 - (afabiani ) - Adding minor fixes as identified by @capooti review of PR #3698 - * [577097a98] 2018-04-03 - (Alessio Fabiani ) Merge pull request #3698 from GeoNode/ISSUE_3684_27X - * [055191022] 2018-03-30 - (afabiani ) [Fixes #3684] Keep original projection and BBOX / general cleanup of resource models and logs - * [1669a8819] 2018-03-30 - (Alessio Fabiani ) Bump GeoServer debian package version to 2.12.2-4 - * [c1b350b30] 2018-03-30 - (afabiani ) - Preparing for RELEASE 2.8.0 : fixing tomcat dependency issues - * [e6dae340b] 2018-03-30 - (Alessio Fabiani ) - geoserver debian package version update - * [79d148042] 2018-03-30 - (Alessio Fabiani ) Merge branch '2.7.x' of https://github.com/GeoNode/geonode into 2.7.x - * [9f55c45a7] 2018-03-30 - (afabiani ) - Preparing for RELEASE RC13 - * [7b6ea8d65] 2018-03-30 - (Alessio Fabiani ) - preparing for release 2.8.0 - * [dfff96887] 2018-03-30 - (Alessio Fabiani ) Merge branch '2.7.x' of https://github.com/GeoNode/geonode into 2.7.x - * [cc48eb028] 2018-03-30 - (afabiani ) - Preparing for RELEASE RC13 - * [e762a709d] 2018-03-30 - (Alessio Fabiani ) Merge branch '1.7.x' of https://github.com/GeoNode/geonode into 2.7.x - * [ac806569f] 2018-03-30 - (afabiani ) - Preparing for RELEASE RC13 - * [2f69c5ef5] 2018-03-30 - (Alessio Fabiani ) Updated changelog for version 2.8rc12 - * [8debdec49] 2018-03-30 - (afabiani ) - Preparing for RELEASE RC12 - * [664dd5cc0] 2018-03-30 - (afabiani ) - Preparing for RELEASE RC12 - * [39b5c9095] 2018-03-30 - (afabiani ) [fixes #3644] Zoom to layer extent zooms to whole world instead #3644 - * [2e28e4b72] 2018-03-30 - (afabiani ) [fixes #3690] Maps metadata is broken - * [33ad61dbf] 2018-03-30 - (afabiani ) [fixes #3690] Maps metadata is brfoken - * [3790b8bab] 2018-03-29 - (afabiani ) - updating deps ppa checks on setup.py - * [6e1a0d14f] 2018-03-28 - (Alessio Fabiani ) Merge pull request #3680 from DanielJDufour/geotiff-io - * [ec51ff60b] 2018-03-28 - (Alessio Fabiani ) Docs update (#3689) - * [f91483f25] 2018-03-28 - (Alessio Fabiani ) Merge pull request #3688 from francbartoli/2.7.x - * [62899c090] 2018-03-28 - (DanielJDufour ) Merge branch 'master' of github.com:GeoNode/geonode into geotiff-io - * [8fc3cf31d] 2018-03-28 - (Francesco Bartoli) Merge remote-tracking branch 'origin/2.7.x' into 2.7.x - * [02ef697e3] 2018-03-28 - (Francesco Bartoli) Fix nginx service as allowed host (#3687) - * [0bdb10f9f] 2018-03-28 - (DanielJDufour ) added integration tests for geotiff.io - * [8a819c252] 2018-03-28 - (DanielJDufour ) [geotiff.io integration] don't add access token param to url if none - * [cb5b82978] 2018-03-28 - (Alessio Fabiani ) Merge branch '2.7.x' of https://github.com/GeoNode/geonode into 2.7.x - * [e5a8a98f3] 2018-03-28 - (afabiani ) - docs update - * [827a02d4a] 2018-03-28 - (Alessio Fabiani ) Merge pull request #3685 from GeoNode/ALIGN_WITH_RC - * [6f766aefc] 2018-03-28 - (Francesco Bartoli) Fix nginx service as allowed host - * [15125a2a6] 2018-03-28 - (Simone Dalmasso ) remove nowrap to fix right columns css issues in detail pages - * [e439c2a3e] 2018-03-28 - (Alessio Fabiani ) Merge pull request #3686 from simod/2.7.x - * [3b1e50564] 2018-03-28 - (Simone Dalmasso ) remove nowrap to fix right columns css issues in detail pages - * [fe1c7259a] 2018-03-28 - (DanielJDufour ) fixed flake8 issues to align with style guide - * [df303bdf2] 2018-03-28 - (afabiani ) - Fix pub geoserver endpoint - * [1d6a5c606] 2018-03-28 - (Alessio Fabiani ) - Fix test cases - * [e4fa47072] 2018-03-28 - (Alessio Fabiani ) - Fix test cases - * [1ce13c6bf] 2018-03-28 - (afabiani ) - Fix pub geoserver endpoint - * [7b0708500] 2018-03-28 - (afabiani ) - Fix csw test cases - * [4f7d82a92] 2018-03-27 - (afabiani ) - Align with Release Candidate - * [6421943bf] 2018-03-27 - (afabiani ) [Before Release 2.7.6] Updating settings and docs - * [517fa4801] 2018-03-27 - (afabiani ) - ReadTheDocs fix - * [d02dec0cd] 2018-03-27 - (afabiani ) [Deps] geoserver-geonode-ext-2.12.2 - * [640b9bdf1] 2018-03-27 - (afabiani ) [Before Release 2.7.5] Updating JS assets - * [e42d0e480] 2018-03-27 - (afabiani ) [Before Release 2.7.5] Updating documentation and version - * [d771fc6d8] 2018-03-27 - (Alessio Fabiani ) Merge pull request #3683 from GeoNode/BKPORT_STABLE_FIXES - * [529daaf0e] 2018-03-27 - (afabiani ) - Backport stable fixes from master branch - * [041d609ca] 2018-03-27 - (afabiani ) Merge branch '2.7.x' of https://github.com/GeoNode/geonode into 2.7.x - * [110ca29fe] 2018-03-27 - (Alessio Fabiani ) Merge pull request #3682 from lucernae/fix_js_assets - * [e46ef1cf7] 2018-03-27 - (Rizky Maulana N..) Fix typo in a variable in search.js - * [9e9c8dce6] 2018-03-26 - (Daniel J. Dufour ) Merge branch 'master' into geotiff-io - * [50da9663c] 2018-03-26 - (DanielJDufour ) added integration with geotiff.io - * [e215cf4a2] 2018-03-26 - (Alessio Fabiani ) Merge pull request #3677 from vasuse7en/2.7.x - * [dcce0f1fb] 2018-03-26 - (Vasu Babu Kandi..) Merge branch '2.7.x' into 2.7.x - * [16fd4fde1] 2018-03-22 - (Vasu Babu Kandi..) remove duplicate logs. This will resolve kartoza/docker-geosafe#98 - * [1e53f9779] 2018-03-20 - (Cezary Statkiew..) Merge pull request #3669 from GeoNode/ISSUE_#3668 - * [ea3cc7443] 2018-03-20 - (Cezary Statkiew..) Merge pull request #3667 from GeoNode/ISSUE_#3665 - * [41cf4ecf3] 2018-03-20 - (Cezary Statkiew..) Merge pull request #3670 from GeoNode/MISC_UTILITIES - * [6301c6779] 2018-03-16 - (Alessio Fabiani ) Merge pull request #3673 from marlowp/ISSUE_#3672 - * [ee01117f7] 2018-03-16 - (Peter Marlow ) Retrieve correct workspace for layer when managing geofence rules - * [26ce5444c] 2018-03-15 - (afabiani ) A bunch of .sh scripts misc utilities - * [3404ec138] 2018-03-15 - (Alessio Fabiani ) Update metadata_form_js.html - * [91895c372] 2018-03-15 - (afabiani ) [Fixes #3668] Metadata Advanced and Wizard return links broken - * [14224842b] 2018-03-15 - (Alessio Fabiani ) [Fixes #3665] http request bad keyword argument layers= - * [c1d4f6ca6] 2018-03-15 - (Alessio Fabiani ) [Ready] Refactor remote services 27x (#3657) - * [d2062f5c8] 2018-03-15 - (Ahmed Nour Eldeen) use layer name and workspace name to update layers - * [b060a873e] 2018-03-15 - (Ahmed Nour Eldeen) handle geofence rules based on layer name and workspace - * [f565571ec] 2018-03-15 - (Alessio Fabiani ) Merge pull request #3660 from GeoNode/3531_celery_module - * [9479604cd] 2018-03-14 - (Alessio Fabiani ) Improve Remote Services URLs on Map Config - * [18fe093e4] 2018-03-14 - (Ahmed Nour Eldeen) use layer name and workspace name to update layers - * [b4f5a2535] 2018-03-13 - (Alessio Fabiani ) GeoNode WMS Remote Services Bindings - * [5e854bcdf] 2018-03-13 - (Alessio Fabiani ) Make use of Remote Services Proxified URL - * [018beb214] 2018-03-13 - (Alessio Fabiani ) Fix keywrods slugs insert and filtering - * [fed2d795c] 2018-03-13 - (Alessio Fabiani ) Fix keywrods slugs insert and filtering - * [ad4c92ae5] 2018-03-13 - (Alessio Fabiani ) fixed tabbed content on map detail page (#3645) - * [17772e55c] 2017-12-15 - (capooti ) Align with Refactor remote services 27x #3657 / restored geonode.celery to geonode.celery_app #3531 - * [debb1181f] 2018-03-12 - (afabiani ) Merge branch '2.7.x' of https://github.com/GeoNode/geonode into 2.7.x - * [d19a73cc4] 2018-03-07 - (Alessio Fabiani ) Merge pull request #3610 from francbartoli/porting-docker - * [c1a13fe97] 2018-03-07 - (Francesco Bartoli) Fix celery start process - * [b825977c9] 2018-03-02 - (Alessio Fabiani ) Merge pull request #3655 from GeoNode/2.7.x-patch-fixture - * [43c2ddeb7] 2018-03-02 - (Francesco Bartoli) Update oauth2 fixture with new redirect uri - * [1c4387057] 2018-02-27 - (Francesco Bartoli) Fix redirect_uri link - * [906f05a44] 2018-02-27 - (Francesco Bartoli) Fix broken list for allowed host variable - * [1bef9bb9f] 2018-02-27 - (Francesco Bartoli) Fix missing comma - * [74787018f] 2018-02-26 - (Francesco Bartoli) Add more variables to stdout - * [213b65d05] 2018-02-26 - (Francesco Bartoli) Fix escaping single quotes and invalid identifier - * [d03c0feed] 2018-02-26 - (Francesco Bartoli) Update allowed_host with occurrence wthout port - * [606b13dc0] 2018-02-26 - (Francesco Bartoli) Fix celery command with correct broker uri - * [c6af19815] 2018-02-26 - (Francesco Bartoli) Fix issue with COMPOSE_HTTP_TIMEOUT value - * [b5743f14e] 2018-02-26 - (Francesco Bartoli) Merge branch 'master' into porting-docker - * [069482f85] 2018-02-26 - (Alessio Fabiani ) [Fixes #3636][Fixes #3639] - Replaces PRs for #3636 and #3639 along with test fixes (#3646) - * [faef3ed70] 2018-02-22 - (Francesco Bartoli) Merge branch 'master' into porting-docker - * [596f1ee26] 2018-02-22 - (Alessio Fabiani ) Merge pull request #3642 from JonDHo/2.7.x-map-detail-fix - * [5e9454da3] 2018-02-21 - (Alessio Fabiani ) - Minor improvements to Backup/Restore settings (#3635) - * [0a5cc8442] 2018-02-21 - (Alessio Fabiani ) - Added few safety checks around; cleaning up a bit settings making it a bit clearer (#3634) - * [1d60418dc] 2018-02-21 - (Alessio Fabiani ) - Minor improvements to monitoring collectors and date parser; few updates to the documentation (#3633) - * [73f551880] 2018-02-21 - (NEXUS\hod135 ) fixed tabbed content on map detail page - * [374a1c369] 2018-02-19 - (Alessio Fabiani ) Merge pull request #3631 from GeoNode/TRAVIS_EMAILS - * [8d247561d] 2018-02-19 - (Alessio Fabiani ) - Adding alessio.fabiani@gmail.com to Travis targets - * [1c3cbea4d] 2018-02-19 - (Francesco Bartoli) Add documention for Docker and Rancher (#3629) - * [4ead9e7ac] 2018-02-16 - (Francesco Bartoli) Merge branch 'master' into porting-docker - * [ed62b6b88] 2018-02-16 - (Alessio Fabiani ) [Fixes #3593] Thumbnail generation issue for layer in non EPSG:4326 projection (#3626) - * [1dad3c984] 2018-02-16 - (Alessio Fabiani ) - Add OAuth2 Headers to Proxy (#3625) - * [32c7d9c2f] 2018-02-16 - (Alessio Fabiani ) [Fixes #3619] Password reset error with 'people' adapter (#3620) - * [ec1f16764] 2018-02-16 - (Alessio Fabiani ) [Fixes #3588] [Monitoring] Protect GeoServer REST endpoint and make requests pass through GeoNode Proxy (#3618) - * [e5e293c5d] 2018-02-15 - (Francesco Bartoli) Merge branch 'master' into porting-docker - * [9da89bf80] 2018-02-15 - (Alessio Fabiani ) Merge pull request #3622 from GeoNode/ISSUE_3621 - * [a80e8e40c] 2018-02-14 - (DBlasby ) Add ability to control thumbnail generation implementation (#3623) - * [c5d7ea5bc] 2018-02-14 - (Alessio Fabiani ) [Fixes #3621] Improve Documents Thumbnail generation... - * [d2c875838] 2018-02-14 - (Francesco Bartoli) Merge branch 'master' into porting-docker - * [459c0ce80] 2018-02-14 - (Alessio Fabiani ) Merge pull request #3617 from DBlasby/thumbnail_different_impls - * [03bf69cfa] 2018-02-14 - (Francesco Bartoli) Merge branch 'master' into porting-docker - * [6a4938496] 2018-02-13 - (dblasby ) Add ability to control thumbnail generation implementation - * [ff260e247] 2018-02-13 - (Alessio Fabiani ) Merge pull request #3616 from tomtl/patch-3 - * [5cee66370] 2018-02-13 - (Tom Lee ) Fix readability - * [52352932a] 2018-02-13 - (Alessio Fabiani ) Merge pull request #3615 from tomtl/patch-2 - * [b71055ab7] 2018-02-13 - (Alessio Fabiani ) [Fixes #3609] Download filter not working (#3611) - * [12dcab9dc] 2018-02-13 - (Alessio Fabiani ) [Fixes #3600] [GNIP] APIs to decouple import and load of GeoNode Client Libraries (#3602) - * [66223abf7] 2018-02-13 - (Tom Lee ) Fix typos - * [eabf6abe6] 2018-02-11 - (Francesco Bartoli) Merge branch 'master' into porting-docker - * [3d142c116] 2018-02-11 - (Alessio Fabiani ) - Instroctions on how to configure GeoNode to login throguh LinkedIn and Facebook (#3607) - * [3be168e8a] 2018-02-11 - (Francesco Bartoli) Fix missing volume for docker socket - * [c4296e687] 2018-02-11 - (Francesco Bartoli) Move geoserver and data dir version to latest - * [af2790853] 2018-02-11 - (Francesco Bartoli) Add container name and restart policy - * [d700b8163] 2018-02-11 - (Francesco Bartoli) Add labels for consumers - * [740dab6ae] 2018-02-11 - (Francesco Bartoli) Replace links with depends_on - * [97cd22c2b] 2018-02-11 - (Francesco Bartoli) Set latest image for geonode - * [11040384b] 2018-02-11 - (Francesco Bartoli) Add support for pdb debug - * [e6ed38862] 2018-01-31 - (Francesco Bartoli) Set LB variables empty for all .env files - * [a98b6d5cf] 2018-01-30 - (Francesco Bartoli) Update makefile with LB logic - * [c62ddf780] 2018-01-30 - (Francesco Bartoli) Add logic to control LB variables - * [85ada3eb6] 2018-01-30 - (Francesco Bartoli) Add control for multiple docker host ip addresses - * [4aa86c4b5] 2018-01-28 - (Francesco Bartoli) Add restart policy for configuration service - * [0fe960a0f] 2018-01-28 - (Francesco Bartoli) Remove useless port directive - * [7347f5e15] 2018-01-28 - (Francesco Bartoli) Replace deprecated volume_from with named volume - * [e26425f4f] 2018-01-25 - (Francesco Bartoli) Add uwsgi timeout lower than default nginx - * [2dfba7004] 2018-01-24 - (Francesco Bartoli) Add log to uwsgi configuration - * [ba2a3017e] 2018-01-21 - (Francesco Bartoli) Add control for celery command - * [784aa216e] 2018-02-11 - (Francesco Bartoli) Add uswgi server for production - * [3a7e9b4a8] 2018-01-20 - (Francesco Bartoli) Fix new function name - * [2627946a9] 2018-02-11 - (Francesco Bartoli) Add restart strategy - * [9ec16d53f] 2018-01-20 - (Francesco Bartoli) Use labels to filter containers - * [33f464c33] 2018-01-20 - (Francesco Bartoli) Introducing labels for filtering - * [45b90cebf] 2018-01-19 - (Francesco Bartoli) Fix replacing underscore with dash - * [5798e8b50] 2018-01-19 - (Francesco Bartoli) Replace deprecated links with depends_on - * [5bd3ecd8e] 2018-01-19 - (Francesco Bartoli) Replace underscore with dash for service name - * [f08ee7331] 2018-01-15 - (Francesco Bartoli) Keep docker start scripts for build command - * [76681378a] 2018-01-15 - (Francesco Bartoli) Remove tasks and entrypoint from history - * [c11bb5d3c] 2018-02-11 - (Francesco Bartoli) Adapt settings for handling datastore and geodata - * [4b56d9e5b] 2018-01-11 - (Francesco Bartoli) Retain default postgres db url - * [f7cae317b] 2018-01-11 - (Francesco Bartoli) Move geonode db variables into .env file - * [d9b7770df] 2018-01-10 - (Francesco Bartoli) Add env variables for geonode databases - * [fc5324c66] 2018-01-10 - (Francesco Bartoli) Add variables for postgres configuration - * [56f8aef5e] 2018-01-10 - (Francesco Bartoli) Format compose syntax - * [8c648989a] 2018-01-09 - (Francesco Bartoli) Add named volumes for db services - * [958f7fbe0] 2018-01-09 - (Francesco Bartoli) Change postgres service name - * [f86884ff3] 2018-01-08 - (Francesco Bartoli) Update siteurl and allowed_host with docker host ip - * [eb2be6d61] 2018-01-08 - (Francesco Bartoli) Fix support for named container - * [b74f68644] 2018-02-11 - (Francesco Bartoli) Use named containers by compose prj variable - * [a30948da4] 2018-02-11 - (Francesco Bartoli) Make docker-compose override happy - * [0603e795e] 2018-02-11 - (Francesco Bartoli) Use new renamed image - * [9d8ece89b] 2018-01-03 - (Francesco Bartoli) Add docker sock also to celery service - * [d8b0bdbc2] 2017-12-30 - (Francesco Bartoli) Fix #3539 - * [bdcd7aef8] 2017-12-30 - (Francesco Bartoli) Get access to docker daemon from api - * [19b3a9a66] 2017-12-30 - (Francesco Bartoli) Remove vars unuseful for geoserver - * [03cfe62c0] 2017-12-28 - (Francesco Bartoli) Split commands for injecting docker host ip address - * [397893f46] 2017-12-26 - (Francesco Bartoli) Add variables with dynamic docker ip address from makefile - * [5ee414684] 2018-02-10 - (Alessio Fabiani ) [Fixes #3591] [Map Client - GeoExplorer] Zoom level seems not to be respected the first time (#3595) - * [d0d167cba] 2018-02-10 - (Alessio Fabiani ) [Fixes #3596] [OWS] GetCapabilities local cache on DJango (#3597) - * [ea322f13c] 2018-02-09 - (Alessio Fabiani ) Merge pull request #3608 from UNSW-CFRC/master - * [909c52418] 2018-02-09 - (Jonathan Doig ) Change IHP-WINS to Geonode on home page - * [63ac2c0bf] 2018-02-09 - (Jonathan Doig ) Merge remote-tracking branch 'refs/remotes/GeoNode/master' - * [9981a1137] 2018-02-08 - (Alessio Fabiani ) Merge pull request #3604 from GFDRR/sort_styles - * [d7694e1f9] 2018-02-08 - (Alessio Fabiani ) Merge pull request #3605 from hishamkaram/patch-5 - * [7f26df88e] 2018-02-08 - (Hisham waleed k..) fix for AttributeError: 'module' object has no attribute 'BASEMAP' - * [65e73458f] 2018-02-08 - (François Van De..) Sort available layer styles - * [d89fe308d] 2018-02-07 - (Alessio Fabiani ) [Fixes #3598] [Pinax Notifications] Upgrade to version 4.1.0 (#3599) - * [b965607fb] 2018-02-02 - (Alessio Fabiani ) Merge pull request #3558 from GeoNode/GNIP_3485_2 - * [80a9dd149] 2018-02-02 - (Alessio Fabiani ) [Foward Port from 2.7.x] - Align on master fixes and improvements for 2.8.0rc11 release (#3584) - * [6441de107] 2018-02-02 - (Alessio Fabiani ) [Foward Port from 2.7.x] - Align on master fixes and improvements for 2.8.0rc10 release (#3583) - * [85c657425] 2018-01-30 - (Alessio Fabiani ) - Cumulative fixes (see list of issues below) - * [1e07581fc] 2018-01-30 - (Alessio Fabiani ) - Cumulative fixes (see list of issues below) - * [b3b71eb73] 2018-01-30 - (Ricardo Garcia ..) made shapefile validation robust to uppercase names - * [7dcc9a4bf] 2018-01-30 - (Ricardo Garcia ..) Restored upload of zip and csv files - * [fa9a892ac] 2018-01-29 - (Alessio Fabiani ) Merge with Social Account - * [16d8d6586] 2018-01-29 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into GNIP_3485_2 - * [71f22f111] 2018-01-29 - (Alessio Fabiani ) Merge pull request #3585 from cartologic/master - * [ed84688f5] 2018-01-29 - (Ahmed Nour Eldeen) solve issue of not setting download perms .["WFS", "WCS", "WPS"] - * [b8b674bcd] 2018-01-28 - (Ahmed Nour Eldeen) Merge pull request #1 from GeoNode/master - * [37be056f9] 2018-01-15 - (Ricardo Garcia ..) added support for KML and KMZ with GroundOverlay - * [103d82558] 2017-12-20 - (Ricardo Garcia ..) removed group invitations to non users - * [247f5bb58] 2018-01-26 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into GNIP_3485_2 - * [af8b4ceb4] 2018-01-25 - (Alessio Fabiani ) - GeoNode-2.7.5.dev20180125135927 - * [f3970dcc0] 2018-01-25 - (Alessio Fabiani ) Updated changelog for version 2.7.5.dev20180124154147 - * [253f5bed6] 2018-01-25 - (Alessio Fabiani ) Merge pull request #3578 from GeoNode/FWPORT_280_RELEASE_STUFF - * [00d0df8ba] 2018-01-24 - (Alessio Fabiani ) Merge pull request #3581 from sebclarke/master - * [a90328597] 2018-01-24 - (Alessio Fabiani ) Updated changelog for version 2.8rc10 - * [8d9e45f93] 2018-01-24 - (sebastianclarke ) Update the DEFAULT_ANONYMOUS_DOWNLOAD_PERMISSION setting to query the correct environment variable - * [c8abc48df] 2018-01-23 - (Alessio Fabiani ) GeoNode 2.7.5.dev20180123130714 - * [d60e7a04e] 2018-01-23 - (Alessio Fabiani ) Updated changelog for version 2.7.5.dev20180123112419 - * [c8bd212df] 2018-01-16 - (Alessio Fabiani ) [Fixes #3565 #3566] Topic Category is no more visible on the Layer summary list / If a Resource belongs to a Group, it should be also visible on the summary list - * [dd4a1975c] 2018-01-16 - (Alessio Fabiani ) [Fixes #3568] - Fix, improve and adapt Backup/Restore Scripts to GeoNode 2.6+ (#3571) - * [4b9a32850] 2018-01-22 - (Alessio Fabiani ) Merge branch 'FWPORT_280_RELEASE_STUFF' of https://github.com/GeoNode/geonode into GNIP_3485_2 - * [9f0525cc9] 2018-01-22 - (Alessio Fabiani ) Merge branch 'master' of https://github.com/GeoNode/geonode into GNIP_3485_2 - * [908199613] 2018-01-08 - (Alessio Fabiani ) - Fix GeoNode 2.6.3 to 2.7.x+ Migration (#3547) - * [a93b6195d] 2018-01-08 - (Alessio Fabiani ) - Fix GeoNode 2.6.3 to 2.7.x+ Migration (#3547) - * [259dffd8b] 2018-01-18 - (Alessio Fabiani ) - Update BR settings.init app modules - * [07549481d] 2018-01-16 - (Alessio Fabiani ) [Fixes #3568] - Fix, improve and adapt Backup/Restore Scripts to GeoNode 2.6+ (#3571) - * [da4b12b92] 2018-01-15 - (Ricardo Garcia ..) added support for KML and KMZ with GroundOverlay - - -- Alessio Fabiani Tue, 21 Aug 2018 16:47:18 +0200 - -geonode (2.8.0+thefinal1) xenial; urgency=high - * [514196] Update README - * [f6dd15] Constrain pip to 9.0.3 - - -- Alessio Fabiani Thu, 26 Apr 2018 12:44:14 +0200 - -geonode (2.8.0+thefinal0) xenial; urgency=high - [ afabiani ] - * [ac8065] - Preparing for RELEASE RC13 - * [cc48eb] - Preparing for RELEASE RC13 - - [ Alessio Fabiani ] - * [7b6ea8] - preparing for release 2.8.0 - - [ afabiani ] - * [9f55c4] - Preparing for RELEASE RC13 - - [ Alessio Fabiani ] - * [e6dae3] - geoserver debian package version update - - [ afabiani ] - * [c1b350] - Preparing for RELEASE 2.8.0 : fixing tomcat dependency issues - - [ Alessio Fabiani ] - * [1669a8] Bump GeoServer debian package version to 2.12.2-4 - - [ afabiani ] - * [055191] [Fixes #3684] Keep original projection and BBOX / general cleanup of resource models and logs - * [de4eaa] - Adding minor fixes as identified by @capooti review of PR #3698 - - [ Alessio Fabiani ] - * [48eea1] Release 2.8.0 - - -- Alessio Fabiani Tue, 03 Apr 2018 10:40:23 +0200 - -geonode (2.8.0+rc12) xenial; urgency=high - [ Alessio Fabiani ] - * [27db87] - Fix GeoNode 2.6.3 to 2.7.x+ Migration (#3547) - * [f3970d] Updated changelog for version 2.7.5.dev20180124154147 - * [af8b4c] - GeoNode-2.7.5.dev20180125135927 - - [ François Van Der Biest ] - * [65e734] Sort available layer styles - - [ DBlasby ] - * [a80e8e] Add ability to control thumbnail generation implementation (#3623) - - [ NEXUS\hod135 ] - * [73f551] fixed tabbed content on map detail page - - [ Francesco Bartoli ] - * [43c2dd] Update oauth2 fixture with new redirect uri - - [ Alessio Fabiani ] - * [c1d4f6] [Ready] Refactor remote services 27x (#3657) - - [ Vasu Babu Kandimalla ] - * [16fd4f] remove duplicate logs. This will resolve kartoza/docker-geosafe#98 - - [ Rizky Maulana Nugraha ] - * [e46ef1] Fix typo in a variable in search.js - - [ afabiani ] - * [529daa] - Backport stable fixes from master branch - * [e42d0e] [Before Release 2.7.5] Updating documentation and version - * [640b9b] [Before Release 2.7.5] Updating JS assets - * [d02dec] [Deps] geoserver-geonode-ext-2.12.2 - * [517fa4] - ReadTheDocs fix - * [642194] [Before Release 2.7.6] Updating settings and docs - * [7b0708] - Fix csw test cases - * [1ce13c] - Fix pub geoserver endpoint - - [ Simone Dalmasso ] - * [3b1e50] remove nowrap to fix right columns css issues in detail pages - - [ Francesco Bartoli ] - * [6f766a] Fix nginx service as allowed host - - [ afabiani ] - * [e5a8a9] - docs update - * [3790b8] - updating deps ppa checks on setup.py - * [33ad61] [fixes #3690] Maps metadata is brfoken - * [2e28e4] [fixes #3690] Maps metadata is broken - * [39b5c9] [fixes #3644] Zoom to layer extent zooms to whole world instead #3644 - * [664dd5] - Preparing for RELEASE RC12 - * [8debde] - Preparing for RELEASE RC12 - - [ Alessio Fabiani ] - - -- Alessio Fabiani Fri, 30 Mar 2018 11:55:49 +0200 - -geonode (2.8.0+rc10) xenial; urgency=high - - * [c8abc4] GeoNode 2.7.5.dev20180123130714 - - -- Alessio Fabiani Wed, 24 Jan 2018 16:41:05 +0100 - -geonode (2.7.5+dev20180123112419) xenial; urgency=high - * [dd4a19] [Fixes #3568] - Fix, improve and adapt Backup/Restore Scripts to GeoNode 2.6+ (#3571) - * [c8bd21] [Fixes #3565 #3566] Topic Category is no more visible on the Layer summary list / If a Resource belongs to a Group, it should be also visible on the summary list - - -- Alessio Fabiani Tue, 23 Jan 2018 14:06:12 +0100 - -geonode (2.8.0+rc9) xenial; urgency=high - - * UNRELEASED - - -- Alessio Fabiani Mon, 22 Jan 2018 17:00:01 +0100 - -geonode (2.8.0+rc8) xenial; urgency=high - - * UNRELEASED - - -- Alessio Fabiani Mon, 22 Jan 2018 16:07:49 +0100 - -geonode (2.8.0+rc7) xenial; urgency=high - - * [d4e678] - GeoNode Releasing 2.8.0rc3 - * [f61c3a] - GeoNode 2.8rc6 - * [82927b] - GeoNode 2.8 - - -- Alessio Fabiani Mon, 22 Jan 2018 15:12:04 +0100 - -geonode (2.8.0+rc6) xenial; urgency=high - - * UNRELEASED - - -- Alessio Fabiani Fri, 19 Jan 2018 02:42:42 +0100 - -geonode (2.8.0+rc5) xenial; urgency=high - - * UNRELEASED - - -- Alessio Fabiani Fri, 19 Jan 2018 01:59:18 +0100 - -geonode (2.8.0+rc4) xenial; urgency=high - - * [b92c19] - GeoNode Releasing 2.8.0rc3 - - -- Alessio Fabiani Fri, 19 Jan 2018 01:17:22 +0100 - -geonode (2.8.0+rc3) xenial; urgency=high - - [ Jeremiah Cooper ] - * [b4da5a] Remove double migrations causing the build to fail. - - [ Alessio Fabiani ] - * [f8b945] - GeoNode Releasing - - -- Alessio Fabiani Thu, 18 Jan 2018 20:39:58 +0100 - -geonode (2.8.0+rc2) xenial; urgency=high - - * UNRELEASED - - -- Alessio Fabiani Thu, 18 Jan 2018 11:34:09 +0100 - -geonode (2.8.0+rc1) xenial; urgency=high - - * UNRELEASED - - -- Alessio Fabiani Wed, 17 Jan 2018 18:50:18 +0100 - -geonode (2.8.0+rc0) xenial; urgency=high - - * UNRELEASED - - -- Alessio Fabiani Wed, 17 Jan 2018 18:45:44 +0100 - -geonode (2.8.0+rc0) xenial; urgency=high - - * [4c8284] Bump to version 2.7.4 - * [6afd8c] [Closes #3528] [Backport 2.9.x stable fixes to 2.7.x] - Prepare for 2.8.0 Release - * [f0be26] Bump to version 2.7.5 - * [a2209d] [Backport to 2.7.x][Issue #2948] - Errors saving metadata keywrods and regions (#3532) - * [57ba99] [Fixes #3533] - GeoServer Proxy creates a wrong URL when dealing with 'workspaces' bounded SLDs (#3535) - * [27db87] - Fix GeoNode 2.6.3 to 2.7.x+ Migration (#3547) - * [a1ed92] [2.7.x] - Prepare for 28x (#3551) - - -- Alessio Fabiani Mon, 15 Jan 2018 09:56:20 +0100 - -geonode (2.7.4+dev20171114153121) xenial; urgency=high - - [ Cezary Statkiewicz ] - * [94e803] monitoring: initial model, tests - * [1ec40c] monitoring: log requests for monitoring - * [998d32] monitoring: catch view errors - * [19e926] monitoring: collecting script, misc updates - * [110026] monitoring: collector - * [f14a90] monitoring: stats calc and storage - * [5d8fc9] monitoring: storage+ metrics api - * [2c0059] monitoring: show metrics cli script - * [ce3760] monitoring: get stats script - * [e22e6c] monitoring: utility scripts improvements - * [9ab22b] monitoring: handle labels in rendering stats - * [d5c2d6] monitoring: data views - * [44688f] monitoring: collect system-level data - * [911984] monitoring: more system metrics - * [efd197] monitoring: storage stats - * [d14738] monitoring: network stats - * [315fd6] monitoring: labels/resources list view - * [50d75f] monitoring: collect_metrics: do not crash on uninited services - * [3635ba] monitoring: switch for clearing old data - * [45e888] monitoring: store network rates, misc improvements - * [d85f78] monitoring #187: calculate aggregated values for rates, collection speedups - * [9e14d2] monitoring: #191 exceptions information in api - * [8037de] monitoring: minor fixes - * [c200ae] monitoring: field lookup error - * [ed8c66] monitoring: log request when regex not match - * [e9f2da] monitoring: logging levels - * [17f920] monitoring: #189 show all labels for metric - - [ Goran Mekić ] - * [f774dd] Initial frontend - - [ Cezary Statkiewicz ] - * [054859] monitoring: #189 more stats within api - * [09ec13] monitoring: #190 ows service in metrics - - [ Goran Mekić ] - * [645930] Unify static file paths - - [ Cezary Statkiewicz ] - * [c08775] monitoring: #190 wxs metrics, #187 corrected response time calculations - * [0bcd84] monitoring: handle geoserver error - * [1fef89] monitoring: missing migrations - * [37d35b] monitoring: error metric names - * [8bce18] monitoring: error stats view - * [36fc1c] monitoring: unicode label handling - * [469f37] monitoring: error stats view - * [a866c0] monitoring: ows services list api view - * [fc93ef] monitoring: default interval/valid period handling without a crash - * [9a21f2] monitoring: #191 expose exceptions in beacon view - * [3f557f] monitoring: #186 system info with psutil - * [28106b] monitoring: code cleanup - * [50fa30] monitoring: #186 process new hw metrics - - [ Goran Mekić ] - * [5e123b] Built frontend files and add graphs - - [ Cezary Statkiewicz ] - * [6b2ecf] monitoring: #193 notifications check model - * [ea9ecc] monitoring: tests updated so they work again - * [01cc7a] monitoring: #198 monitoring view in geonode template - * [608b57] monitoring: #193 notifications check models - * [06a114] monitoring: #193 notifications check per resource - * [770eb5] monitoring: #193 notifications check per resource - * [bfcf90] monitoring: #193 notifications per user - * [50db9b] monitoring: #193 notifications list view - - [ Goran Mekić ] - * [0b03d0] Fix CORS in dev, add actions - - [ Cezary Statkiewicz ] - * [44d1bc] monitoring: #193 notifications tests, moved user to metric notifications - * [8ec92e] monitoring: #193 notification modify views - - [ Goran Mekić ] - * [1ab759] Unify frontend and backend index.html template - * [66f258] Make react play nice with existing app - * [8705d7] Style footer like the rest of the site - * [7832a3] Position react app on the page - * [e9ef80] Set backend URL dymanically - * [9d3c47] Fix math in response times - - [ Ismail Sunni ] - * [b64802] Enable ascii file upload. - * [5fef1b] Unit test for ascii file. - * [a9c4cd] Add ASCIIs to the error message. - - [ afabiani ] - * [40709a] - Fix wrong REGISTRATION OPEN flag lookup - - [ Alessio Fabiani ] - * [c5a1f4] Align requirements to "geonode_user_messages==0.1.6" - * [df35b0] Update setup.py - - [ Goran Mekić ] - * [6a86e9] Fix for ISSUE #3147: Display form errors - * [fae7a7] Fix for ISSUE #3149: trying to change the Language - * [73399f] Fix for ISSUE #3150: more fields to batch update (#3164) - * [a747af] Hardware page - - [ Cezary Statkiewicz ] - * [23d9eb] monitoring: use timedelta for interval - - [ Goran Mekić ] - * [32e43f] Make header interval clickable - * [c002f9] Calculate header intervals - * [8b2ba2] Format dates in URL - * [5cc96c] Display software performace data - * [39f258] Proper theme and data handling - * [1b1b41] Get CPU usage from API - * [eccf1d] Reorganize actions and add mem.free stats - - [ Cezary Statkiewicz ] - * [1adf10] monitoring: show service type - - [ Goran Mekić ] - * [2d55e8] Reorganize actions and add mem.free stats - * [978d62] Render first graph - * [639fe0] Format displayed memory - * [b6e8a4] Add monitoring item to menu - * [cef00d] Fix initial API values fetch - * [fb96ae] Build monitoring frontend static files - * [f5aa0c] Show monitoring only to superusers - * [5ed034] Add spinner - * [42df0f] Round seconds and miliseconds - * [ac5ee7] Set graph resolution based on interval - * [07151f] Finish geonode graphs - * [be0023] Display stats on graphs - * [7677cd] Display hardware graphs - * [44f4ec] Get CPU data - * [8b3ccf] Add uptime and autorefresh (not active, yet) - - [ Cezary Statkiewicz ] - * [67dc77] monitoring: #199 collect cpu.usage data - * [b96e4a] monitoring: safer request cleanup in middleware - * [4eb5e8] monitoring: #187 save samples count in metrics values - * [a33166] monitoring: #187 use samples in stats calculation - - [ Goran Mekić ] - * [a63e16] Enable auto refresh on all components - * [94bd42] Fix for ISSUE #201: Raise 404 if no such service - * [ffec67] Implement and display CPU percents - * [1943ea] Calculate percents from collector data - - [ Cezary Statkiewicz ] - * [ccb2cd] monitoring: #203 exceptions api - * [794e1b] monitoring: #187 calculate metrics from requests queryset - * [848164] monitoring: #187 resources in per-ows metrics - * [1d8770] removed code from region changes - * [02aa26] monitoring: frontend - use last data item in series of data, monitor non-get requests - * [cd92dd] monitoring: #200 expose axis label for metric - * [5aad83] monitoring: tests updated - * [013015] monitoring: #193 notifications api - * [c2b8b6] monitoring: notifications emit #193 - * [bb0d8b] monitoring frontend: code cleanup, no console.log - * [c9082b] monitoring: urls in namespace, sys mem unit to bytes - * [0ba2b5] monitoring notifications config - * [b18c78] monitoring: change metric type for some metrics - * [f686f1] monitoring: .check() -> .check_for() to avoid clash with django model.check() - - [ Goran Mekić ] - * [07db51] DropDown for selecting WS services - * [4673cf] Display WS stats - * [4f8eb6] Make autorefresh global - * [e7c114] Display GeoServer stats - * [4bbf7a] Explicitly request GeoNode stats - * [b805c2] Fix typo - * [a5f304] Fix layer upload - * [07d6c5] Add error list page - * [1e619c] Error detail page - * [31f43e] Remove service argument from GET - * [bb1c64] Align elements and remove not-needed ones - * [1c7594] Display error data - * [ee3405] Choose data from API based on interval data - * [deacb7] Display spinner when needed - * [6d80e9] Format URL in error detail - * [af5f25] Style error detail page - * [17c9ba] Shorten data in client section - * [5b9253] Format date on error detail page - * [d3182e] Example react-leaflet - * [2780ba] Add leaflet - * [820eed] Disable header buttons when needed - * [f951e9] Proxy to backend to avoid CORS - * [a6f365] Make a response table - * [9b9652] Alerts page - * [5c1685] Add layer API calls - * [0d5446] Make header of the map - * [b62711] Map without zoom/pan - * [9604d9] Update frontend deps - * [3e7706] Legend and tooltip positioning - * [a6bdde] Add map style - * [876555] Make 10 groups - * [5488ac] Fix syntax in monitoring frontend - * [2556f1] Show data in popup - - [ Cezary Statkiewicz ] - * [a0f439] monitoring: #221 service_type in metric data - * [c56b9e] monitoring: separate metric type for num values - * [686564] monitoring: #225 3-letter country code - * [bb4a17] monitoring: #223 return last X seconds of data - * [0358fa] monitoring: #222 align periods to interval - - [ Goran Mekić ] - * [01aec1] Use last and interval arguments for time - * [8624e5] Get server time for from/to - - [ Cezary Statkiewicz ] - * [e354b0] monitoring: query filter lookup error - - [ Goran Mekić ] - * [23dbb4] Make spinner in header smaller - * [6a635e] Render header dates properly - - [ Cezary Statkiewicz ] - * [3face5] monitoring: #223 return last X for resources, exceptions, labels - - [ Goran Mekić ] - * [6737eb] Draw all countries with the same color - * [f78e2d] Center map - * [e9f11d] Calculate data for the legend - * [56262a] Fix map legend calculation - * [95ca0d] Fix country color calculation - * [0cf07e] Treat specially the high value on the legend - * [8f316e] Add title to map - - [ Cezary Statkiewicz ] - * [8eb286] monitoring: #193 notifications - glued with sending, tests update - * [350303] monitoring: #193 notifications config - * [c3b798] monitoring: #193 notifications config, tests - * [e0bc36] monitoring: #193 #192 notifications - * [8c3f20] monitoring: #193 notifications - sending grace period, improved tests - - [ Goran Mekić ] - * [4613da] Filter - - [ Cezary Statkiewicz ] - * [1b5dc7] monitoring: minor tests updates - * [31cb0f] monitoring: api normalization - * [f94b50] monitoring: #230 notification check severity - * [6f9e00] monitoring: use last period for notifications - * [077af4] monitoring: #227 autoconfigure based on settings - * [6c1d00] monitoring: do reload after autoconfigure - - [ Goran Mekić ] - * [8fbf12] Add alerts settings page - * [4884ee] Get alert data from API - * [bb01b5] Calculate uptime - * [85dd0f] Show alert and error number - * [c2824b] Use right API for alert count - * [91cd8b] Add layer select dropdown - * [a4afa0] Use API data for layer analytics - - [ Cezary Statkiewicz ] - * [245b3c] monitoring: #193 notifications status api improvements - - [ Goran Mekić ] - * [3291da] Show decimals for CPU/Mem metric for values less than 1 - - [ Cezary Statkiewicz ] - * [1ce36c] monitoring: notifications admin, fixtures - * [5868d9] monitoring: notification fixtures - - [ Goran Mekić ] - * [05fcf5] Limit dropdown width - * [248598] Parse alert data from backend - - [ Cezary Statkiewicz ] - * [113163] monitoring: notifications alert format - * [0875db] monitoring: alerts messages - * [28f005] monitoring: fixtures update - - [ Goran Mekić ] - * [87265c] Format alert message - * [1ba5f7] Alert list spinner - * [223cda] Fix alert number stat - - [ Cezary Statkiewicz ] - * [09b0a9] monitoring: notification msg update - - [ Goran Mekić ] - * [b21821] Update alerts and errors on interval change - - [ Cezary Statkiewicz ] - * [25e3cc] monitoring: recompiled bundle.js - - [ Goran Mekić ] - * [6c2c2b] Make W*S and layer selection independent - * [8bff1d] Display alert description - - [ Cezary Statkiewicz ] - * [644882] monitoring: use threshold value for alert msg - * [9d6867] monitoring: alert template - * [a2e70c] monitoring: ows admin - * [6e7cae] monitoring: unit in notification config, #245 remove old data by default in collect_metrics - * [49ef4b] monitoring: #244 expose grace period in api - - [ Goran Mekić ] - * [c4158b] Change health check color with alerts/errors - * [285a64] Alert color depends on severity - * [d2896b] Remove map from software performance - * [eb534a] Position geonode/geoserver stats horizontaly - * [d6f604] Show hostname instead of service name - * [80cf85] Make selected host unique to all pages - - [ Cezary Statkiewicz ] - * [4b9a06] monitoring: #243 error message formatting, timestamps, description of metric - * [41017d] monitoring: notifications config api update - - [ Goran Mekić ] - * [bcbf7c] Make alert list - - [ Cezary Statkiewicz ] - * [06c35f] monitoring: current_value is always an object, not plain value - * [299134] monitoring: current_value is always an object, not plain value - - [ Goran Mekić ] - * [925ab3] Alert config page - * [15c825] Re-enable auth - - [ Cezary Statkiewicz ] - * [34504f] monitoring: disable csrf for notification config - - [ Goran Mekić ] - * [8cca83] Make redux debugging easier - * [b550f3] Render and verify dynamic input - - [ Cezary Statkiewicz ] - * [282f84] monitoring: #234 group by resource, ordering in metric data - * [b2adff] monitoring: #248 notifications config post with json - - [ Goran Mekić ] - * [243408] Send cookie with request - * [e87296] Fix typo - - [ Cezary Statkiewicz ] - * [5c1463] monitoring user docs - * [21793d] monitoring: exception list ows name - * [599bf9] monitoring: docs update - * [2b57c9] monitoring: notification emails in get config - - [ Goran Mekić ] - * [47d5b2] Save dynamic fields - * [e55cb4] Handle value change - - [ Cezary Statkiewicz ] - * [b87563] monitoring: errors in notification saving - - [ Goran Mekić ] - * [08d2d0] Handle non-existing current_value - * [75c0d2] Save active state - * [ad2f30] Handle emails - - [ Cezary Statkiewicz ] - * [9f41aa] monitoring: handle empty fields - - [ Goran Mekić ] - * [c310cf] Handle values close to zero - * [487d44] Handle default values for mail and current_value - * [c81bf8] Use dropdown for steps - * [34f485] Display alert time - * [7324f0] Get frequent layers - - [ Cezary Statkiewicz ] - * [6955c6] monitoring: user documentation update - * [bca353] monitoring: return resource id when metric data is grouped - - [ Goran Mekić ] - * [87777f] Style frequently accessed layers - * [2cf75f] Fix dev config - - [ Cezary Statkiewicz ] - * [2696a0] monitoring docs - * [f02b79] improved ows links app: csw and homepage - * [74cc92] monitoring: process geoserver hw metrics #197 - * [2b2be1] monitoring: updated bundle.js - * [8684f1] monitoring: process geoserver hw metrics #197 updated - - [ Goran Mekić ] - * [943705] Filter for hostgeoserver - - [ Cezary Statkiewicz ] - * [36e2aa] removed duplicate requirements - * [a03357] monitoring docs: ghc integration note - * [e55f95] use gettext_lazy for some strings used before i18n infra is up - * [266110] monitoring: updategeoip command - * [2529f6] monitoring docs update - * [f74ea7] monitoring: test fixes - * [f3ad58] log queries for tests - * [d03060] log queries for tests - * [56f64c] csw backend: check if not sqlite corrected - * [8dbed3] remove overlogging from messaging - * [194636] spatialite support - * [796122] spatialite support (trusty) - * [43c76b] monitoring: removed artifacts from spatialite - * [1637c8] monitoring: removed artifacts from spatialite - * [6c1000] monitoring: flake8 updates - * [0485cb] monitoring review fixes - * [399e67] removed monitoring frontend license - * [4adeac] removed monitoring frontend readme - * [1d540b] monitoring: refresh geoserver metrics, hw labels with hosts - * [047dc9] monitoring: separated geonode/geoserver hw charts #257 - - [ David Sampson ] - * [06155f] line 12, This > These - - [ Cezary Statkiewicz ] - * [0b3a9b] monitoring: global_settings import replaced - * [01625b] monitoring: db logging removed - * [1e2008] updated django-cors-headers deps in setup.py - - [ Alessio Fabiani ] - * [b89280] - Cumulative patches for fixing: GeoExt SLD Editor, Layer SLD Upload, Layer Upload, Document Upload, Layer Metadata Editor, Document Metadata Editor - * [294885] - GeoServer 2.12.x - * [a1ea03] - Cumulative patches for fixing: GeoExt SLD Editor, Layer SLD Upload, Layer Upload, Document Upload, Layer Metadata Editor, Document Metadata Editor - * [ebd0cc] - Cumulative patches for fixing: GeoExt SLD Editor, Layer SLD Upload, Layer Upload, Document Upload, Layer Metadata Editor, Document Metadata Editor - - [ Cezary Statkiewicz ] - * [77dba0] monitoring: consistent available memory indicators - * [050e6f] replace pyformat for sqlite raw queries - * [dc65d1] flake8 checks - - [ Alessio Fabiani ] - * [9b391f] Merge branch 'PATCHES_OCT17_001' of https://github.com/GeoNode/geonode into monitoring - * [01c5f1] Merge branch 'PATCHES_OCT17_001' of https://github.com/GeoNode/geonode into monitoring - - [ Cezary Statkiewicz ] - * [11163d] monitoring: do not initialize geoip db in module - * [49aaff] monitoring: lazy initialize geoip - - [ Alessio Fabiani ] - * [24c08c] - Bump to version 2.7.2 - * [48e26e] - geonode user accounts 1.0.15 - * [68b723] - Bump to version 2.7.3 - * [96900f] Update .travis.yml - * [ac803e] - geonode user accounts 1.0.15 - * [fd7a69] - Improve GeoExt base GIS viewer; adding 'wrapDateline' option and update OL2 version - * [50f99b] - Intercept and handle correctly resource not found exception - - [ Cezary Statkiewicz ] - * [585bcc] monitoring: do not align time window when viewing data, minor fixes in collector - * [d506da] monitoring: request events service in admin - - [ Alessio Fabiani ] - * [0ce219] - Intercept and handle correctly resource not found exception - - [ Cezary Statkiewicz ] - * [79fb56] monitoring: adjusted memory charts and collected mem.used data - * [4410c5] monitoring: updated bundle.js - * [0b52b3] monitoring: corrected url for mem metrics - - [ Alessio Fabiani ] - * [159c47] - Group Managers and Staff Members can now Moderate resources before publishing them - * [250352] - Group Managers and Staff Members can now Moderate resources before publishing them - * [947705] - Group Managers and Staff Members can now Moderate resources before publishing them - * [95d8bc] - Group Managers and Staff Members can now Moderate resources before publishing them - * [f2582c] - Group Managers and Staff Members can now Moderate resources before publishing them - - [ gpetrak ] - * [5bf7d1] master:Adding Greek code 'gre' into Language field in admin layer's form - - [ Alessio Fabiani ] - * [8494f6] - Fix list of groups on Resources Metadata - * [da251d] - Filter out *non* active users from public views and lookups - * [df4505] - Fix an issue on catalogue APIs when looking for Public Groups IDs (#3356) - - [ Andreas Trawoeger ] - * [9f59f1] Fix dateformat sniffing. - - [ gpetrak ] - * [d6c510] Adding charset as option in importlayers command - - [ Rizky Maulana Nugraha ] - * [008980] QGIS Server Related PR to upstream. - - [ Alessio Fabiani ] - * [39c2ae] - Fix an issue with Angular not working with spans and divs - * [b662aa] - Minor improvements to the Group associated to resources: show the title instead of slug / filter on own and public groups only - - [ gpetrak ] - * [496c8c] Fix the ERROR: ResourceBase has no field named 'charset' - * [9b8614] fixing formatting issues - - [ Alessio Fabiani ] - * [82151a] - Issue GWC Invalidation for the affected Layer at SLD change / update - - [ George Petrakis ] - * [965f6e] update the importlayers' docs (#3369) - - [ Alessio Fabiani ] - * [536b02] - Admin and Staff members should be able to see all resources anyway - * [5fa397] - Display Group Profile Titles on Info page instead of Group Slugs - * [eb33d7] - Fix flake8 formatting - - [ capooti ] - * [94be8e] Add sync_geofence command, which syncronize permissions from GeoNode database to GeoServer/GeoFence - * [94b3b6] Fixes #3376 - - [ Francesco Bartoli ] - * [069c0f] Add codecov badge and fix syntax errors - * [06f941] Remove no longer used config.xml - * [9ea55c] Add paver setup item to gitignore - - [ Alessio Fabiani ] - * [d11cdf] Update tests.py - * [495bce] Update tests.py - * [8246a1] Update tests.py - * [9660b7] Update tests.py - * [83b377] - Improve SLDs validation and name extraction - - [ Rizky Maulana Nugraha ] - * [8fba21] QGIS Server Related PR: GeoNode code improvements. - - [ Alessio Fabiani ] - * [312fec] Default CRS, with GeoExplorer plugin, must be EPSG:900913 - * [ec138f] - Recursively parse Hyerarchical Kaywords - * [9aab1a] - Refresh assets JS and CSS with recently updated libraries - - [ Francesco Bartoli ] - * [5e9e31] Fix #3388 and travis services test failing - - [ Alessio Fabiani ] - * [5b2141] - Improvements and tests on Group Activity Pages (#3384) - - [ capooti ] - * [285958] Removing a duplicate in requirements which is causing an error on pip install - * [d3f760] Handles empty titles and missing thumbnails in resource api - - [ Ricardo Garcia Silva ] - * [ccca1b] added img tag with group logo in the frontend (#3395) - * [ec216b] Added tests to assert presence of logo urls in list view of groups - - [ Ariel Nuñez ] - * [402750] No pdb - * [ed3c83] Removed pdb from requirements - - [ Alessio Fabiani ] - * [25aa8a] Update version of "geonode user accounts" to 1.0.15 (#3349) - - [ Paolo Corti ] - * [415b4b] Update instructions for translating GeoNode user interface (#3401) - - [ Alessio Fabiani ] - * [d2c810] Fixes and Improvements Groups and People Search Filters by Name (#3402) - - [ kappu ] - * [911f7c] Fixed geonode layers analytics and w*s layers analytics - * [f39d23] Fixed layer id in layers selector - - [ Cezary Statkiewicz ] - * [c8298b] flake8 fixes - - [ Alessio Fabiani ] - * [eb374b] - Fixes and Improvements to the GeoExplorer Mapfish Printing plugin: improved legends support, being able to correctly handle auth_token on Composer LayerTree - - [ Julien Acroute ] - * [c5f5f9] Use osm source for search map and use protocol relative link http/https - * [4f538e] use regular link to osm copyright page - - [ Alessio Fabiani ] - * [adde19] [Minor] Hardening checks on missing thumbnail APIs (#3412) - - [ cryst10 ] - * [22a7f0] changed from release to stable - * [f580c7] update gdal - - [ capooti ] - * [58825e] Implement enhanced WMS capabilities - - [ Alessio Fabiani ] - * [e7dd0b] [Updates] Replace default crs with 3857 (#3417) - - [ capooti ] - * [a2d6fe] Fixes smoke tests - - [ Alessio Fabiani ] - * [67b32d] Avoid storing access_token (which have expiration) into the map context, and inject them dinamically instead (#3422) - - [ Jeremiah Cooper ] - * [ab041b] Fix docker builds (#3423) - - [ Alessio Fabiani ] - * [a1fd6e] Merge branch 'master' of https://github.com/GeoNode/geonode into 2.7.x - * [51ded1] - Fix Tests Indentation - * [dd1dbb] [Regression] Fix a regression lost during merge: Users should be always able to access their own Resources (#3427) - * [b7941b] [Regression] Fix a regression lost during merge: Users should be always able to access their own Resources (#3427) - - [ capooti ] - * [96de8d] Fixes #3425 - - [ Alessio Fabiani ] - * [e18af5] Merge pull request #3431 from capooti/issue-3425 - * [f8048c] - Relax base_file checks: Unchecked GDAL Info failure - * [101219] [Before Release 2.7.4] Merge with master - - [ Ricardo Garcia Silva ] - * [e814b9] [Before Release 2.7.4] Merge with master - - [ Alessio Fabiani ] - * [20f27a] Allow sending messages to multiple users and groups - - -- Alessio Fabiani Tue, 14 Nov 2017 17:13:40 +0100 - -geonode (2.7.1+dev20171013111656) xenial; urgency=high - - [ Alessio Fabiani ] - * [084a43] - Add the possibility of uploading an arbitrary SLD to a Layer - - [ Simone Dalmasso ] - * [8fd14a] drop alpha version in favour of unstable. reflects the release versions agreement where unstable will be only for odd versions. - * [99d82c] make sure get_version is happy with the unstable flag - * [a29591] bump version to 2.9 unstable - - [ Alessio Fabiani ] - * [c718c3] - Add the possibility of uploading an arbitrary SLD to a Layer - - [ capooti ] - * [6231be] Adding the createlayer contrib application, as per GNIP #3269 - - [ Francesco Bartoli ] - * [649c38] Fix rst syntax errors on setup centos 7 (#3274) - - [ Paolo Corti ] - * [9897a6] Adding the createlayer contrib application, as per GNIP #3269 (#3278) - - [ Alessio Fabiani ] - * [b7ad02] - Fix dev dependencies for OWSLib and django-user-accounts on requirements.txt and setup.py (#3281) - * [aa10da] - Fix SLD Upload issue with Create Layer functionality - * [b302ee] - Fix dev dependencies for OWSLib and django-user-accounts on requirements.txt and setup.py (#3281) - - [ karakostis ] - * [280ca9] changes in the libraries in setup.py and requirements.txt - - [ Alessio Fabiani ] - * [bd9561] - Fix SLD Upload issue with Create Layer functionality - * [5e5921] - Add default port 80 host to 'default_oauth_apps.json' also - * [52f29d] - Avoid exception on User Account deletion - * [eaf0e5] - Specify 'django-user-accounts' egg in order to avoid requirements error during setup on clean environment (#3289) - * [8c5020] - Update Pinax Notifications to v.4.0.0, fix templates and enable them by default - * [28667a] - Add default port 80 host to 'default_oauth_apps.json' also - * [141e88] - Avoid exception on User Account deletion - * [a0f2b6] - Specify 'django-user-accounts' egg in order to avoid requirements error during setup on clean environment (#3289) - - [ karakostis ] - * [9d404b] download_layer - - [ Alessio Fabiani ] - * [e5aa63] Merge pull request #3290 from GeoNode/pinax_upd - - [ Cezary Statkiewicz ] - * [780710] improved ows links app: csw and homepage - - [ capooti ] - * [7a0d5f] Adding the datastore shards contrib application - - [ Cezary Statkiewicz ] - * [6e0cb0] improved ows links app: csw and homepage - - [ Alessio Fabiani ] - * [eea064] - Update OWSLib version to 0.15.0 (#3296) - * [f4239b] - Update OWSLib version to 0.15.0 (#3296) - - [ capooti ] - * [05b044] Merging requirements with master - - [ Jonathan Doig ] - * [426e08] Mention ISO and describe CSW in developer page - * [f04988] Fixed typo "for" not "from" - - [ capooti ] - * [1a90e8] Adding the datastore shards contrib application - - [ Ami Rahav ] - * [0bcdce] Version constrain six, see https://github.com/benjaminp/six/issues/210 - - [ karakostis ] - * [e0e2d6] remove unnecessary code - - [ Paolo Corti ] - * [a5882a] Add a link to create-layer in profile page (#3301) - - [ karakostis ] - * [b5f8e3] adding max features and clear button - - [ Glenn Vorhes ] - * [04f635] gitignore celery helper files - - [ Paolo Corti ] - * [cc0234] Add a link to create-layer in profile page (#3301) - - [ karakostis ] - * [240e15] Download a layer filtered by attributes - - [ GeoSolutions ] - * [914a55] - Fix GeoFence rule creation when priority is not continuous - - [ Alessio Fabiani ] - * [7a68cb] - Fix GeoFence rule creation when priority is not continuous - - [ Glenn Vorhes ] - * [9f5246] gitignore celery helper files - - [ Alessio Fabiani ] - * [9bce2a] - Fix GeoFence rule creation when priority is not continuous - - [ Francesco Bartoli ] - * [f9f45a] Fix #3308 - * [ce68d0] Fix #3308 (#3309) - - [ Alessio Fabiani ] - * [9cf92a] Merge pull request #3305 from GeoNode/GEOFENCE_FIX - - [ Francesco Bartoli ] - * [289f5a] Fix malformed value for ast - - [ Alessio Fabiani ] - * [ee5fef] - Avoid Multiple Tags Exception - - [ capooti ] - * [fe2a08] Add the "Add the layer to an existing map" feature in layer detail page - - [ Alessio Fabiani ] - * [3b1d7f] - Master branch alignment - - [ Jose ] - * [2081ab] Message colors fix. - - [ Omar Ureta ] - * [934963] Added additional instructions - - [ Cezary Statkiewicz ] - * [e2909e] async notifications - updated, improved - * [aa3e82] async notifications - updated, improved - - [ Alessio Fabiani ] - * [49ec1e] - Master branch alignment (#3318) - - [ Cezary Statkiewicz ] - * [eea80f] async signals docs #2889, #3307 - * [a39b57] pinax notifications docs update #2889, #3307 - * [fb86c3] pinax notifications docs update #2889, #3307 - - [ Alessio Fabiani ] - * [801511] - Avoid Multiple Tags Exception - - [ karakostis ] - * [4ebba3] updated gitignore - * [00aeff] small fixes - * [9f7d17] pep8 syntax issue fix - * [ab2509] proposed minor fixes - - [ Alessio Fabiani ] - * [78fc38] - Make Pavement GeoServer setup/reset command checks oauth2 filter also - * [0c903f] - Make SLD Upload method checking fo UserLayer node also - * [06c1a2] - Jetty Runner Update Version - - [ karakostis ] - * [633482] refactoring download filtered layer - - [ Alessio Fabiani ] - * [a7104a] - Reduce logs verbosity of consumer when logger level is 'INFO' - - [ Paolo Corti ] - * [b82e0c] Fixes #3337 (#3339) - * [d4140c] Fixes #3332 (#3333) - - [ karakostis ] - * [db95c2] fixes - * [5c719b] blank line fixes - * [d7ef63] fix js error - - [ Paolo Corti ] - * [fa416f] Fixes #3341 and provide a setting to enable/disable email display in profiles (#3342) - - [ Alessio Fabiani ] - * [0b3a4c] - Cumulative patches for fixing: GeoExt SLD Editor, Layer SLD Upload, Layer Upload, Document Upload, Layer Metadata Editor, Document Metadata Editor - - [ Tyler Battle ] - * [8826ba] GeoServer stores weren’t being created automatically - - [ Alessio Fabiani ] - * [66d4b0] - Improved Thumbnail Feedbacks - * [8bf09a] Merge with master - - -- Alessio Fabiani Fri, 13 Oct 2017 17:14:40 +0200 - -geonode (2.7.0+thefinal0) xenial; urgency=high - - [ afabiani ] - * [97107b] PR for (#2408) GNIP: GeoNode Metadata Editor Improvements - - [ Seno @ ThinkPad ] - * [a3968d] fix: #2858 KeyError on logout when using qgis local_settings.py sample - - [ Simone Dalmasso ] - * [b1254a] update release version (#2894) - - [ Jeffrey Johnson ] - * [79135f] fix requirements.txt - - [ Martina ] - * [bcdc7e] Fix issue with Leaflet library - - [ Jonathan Doig ] - * [dbe2e1] Removed uploaded/layers from Apache config - - [ Martina ] - * [20a02f] LAYER_PREVIEW_LIBRARY set to geoext as default - - [ Simone Dalmasso ] - * [dedd93] deny uploaded/layers access (#2900) - - [ Alessio Fabiani ] - * [ffbcdc] Update quick_install.txt - * [472cdd] Update quick_install.txt - * [87b5fe] Update quick_install.txt - - [ Simone Dalmasso ] - * [595023] make sure metadata detail is generalised - - [ afabiani ] - * [d7619f] - PR fixes - - [ Way Barrios ] - * [3a7b33] notification refactor implementation - - [ Simone Dalmasso ] - * [30405b] Updated changelog for version 2.6a1 - * [ca2e9d] update release version - * [479ed7] update release version for 2.7 - - [ Alessio Fabiani ] - * [395da6] PR for ISSUE #2910 - Allow contrib apps to extend site base (#2912) - - [ Simone Dalmasso ] - * [fac9e2] add tokenfield to assets.min.js - - [ Paolo Pasquali ] - * [0bf6c6] Fiox meta-categories in home page - * [8be3dc] Remove truncatechars in Resource Base Info Panel Abstract - * [52bf90] Fix tabs in Layer Detail Page - * [fe7667] Fix togglable tabs in detail pages - * [dd6599] Fix issue #2783 Announcements getting covered by the navbar in the index page - * [8e97e4] Enlarge people card - - [ Simone Dalmasso ] - * [dbc88b] keywords and regions default should be list and not tuple - - [ Ariel Nuñez ] - * [d36203] Install geonode in docker - - [ Way Barrios ] - * [6a12eb] update settings for Docker use - - [ Ezequiel Gonzalez Rial ] - * [7177f1] Remove the 3D viewer tool from maps - * [a3957d] Remove the 3D viewer tool from maps - - [ Ariel Nuñez ] - * [c5b9a8] Fix Travis error (log size was too big) and speed up test suite - - [ Ezequiel Gonzalez Rial ] - * [945eb8] Change Geoexplorer aboutUrl - * [13a219] Change Geoexplorer aboutUrl - - [ Paolo Pasquali ] - * [be28a3] Add style to comment section and Page Detail About section (#2931) - - [ Alessio Fabiani ] - * [ca5ce5] Update metadata_base.html - * [e18759] Update metadata_detail.html - - [ afabiani ] - * [9bccd3] Add missing templated for issue #2910 - Allow contrib apps to extend site base - - [ Ariel Nuñez ] - * [52520a] Upgrade pavement to create notice types - * [b8d588] Added create_notices function for tests - * [3e8559] Use create_notices on tests - * [f2e25d] Restrict incompatible versions of kombu - * [fc02ca] Added notifications to maps - - [ Alessio Fabiani ] - * [31f762] Improvements for PR (#2408) GNIP: GeoNode Metadata Editor Improvements (#2935) - - [ Ariel Nuñez ] - * [667e50] Added create_notifications to social tests and improved populate data functions - * [9be40c] Disabled test that uncovers a recursive dependency - * [7cf262] Added tests placeholder for messaging - - [ gonrial ] - * [dd70be] Added {{block.super}} to all {% block extra_script %} (#2940) - * [ff80d8] Added {{block.super}} to all {% block extra_script %} (#2939) - - [ Ariel Nuñez ] - * [055861] Remove hardcoded wait - * [8ad4ec] Added oauth fixtures to integration tests - * [48465b] Added messaging to pavement.py - * [d56b86] Move start_messaging after syncdb is done - - [ glennvorhes ] - * [f11a57] bump paver version - - [ Adam Lawrence ] - * [7dedda] Add extra information for Ubuntu install - - [ Ariel Nuñez ] - * [d522de] Added manual geoserver post_save call to the integrtion tests - - [ Adam Lawrence ] - * [def12d] Revise quick install instruction - - [ afabiani ] - * [acab55] - Improve geoserver_post_save2 signals - * [955740] - Improve geoserver_post_save2 signals - - [ Ariel Nuñez ] - * [f041fe] pep8 for social/signals.py - * [4209d8] pep8 security/views - - [ afabiani ] - * [c277bc] - pep8 fixes - - [ Ariel Nuñez ] - * [8fc3e0] pep8 fixes - * [abca5f] Added integration tests to coverage - - [ Paolo Corti ] - * [17993c] Change legend in layer detail page when the style is changed by the user (#2944) - - [ capooti ] - * [307dbb] Fixing a typo from previous commit - - [ Paolo Corti ] - * [67b774] Fixes some style stuff (#2950) - - [ afabiani ] - * [8aceef] - Minor updated to the Windows bin installer doc page - - [ travis ] - * [fca9cc] Correctly handle file names with spaces or parentheses - * [519bd3] Handle OS X zipped files in upload - - [ George Petrakis ] - * [70e10d] Remove 3d viewer tool from GeoExplorer (#2943) - - [ Alessio Fabiani ] - * [755e74] - Add support for big ZIP files and being sure the target folder is cleaned - * [d32a09] [Backport 2.6.x] - Add support for big ZIP files and being sure the target folder is cleaned - - [ afabiani ] - * [85777f] [ISSUE #2914] - User is still authenticated in geoserver after logging out from geonode - * [011e04] - GeoServer returns directly SLD Body as HttpResponse - - [ travis ] - * [83f9c9] Fixed incorrect DOM name to create geogig store - - [ afabiani ] - * [fc0179] - fix metadata completeness counter - - [ travis ] - * [98e321] Make inbox message deletion compatible with IE - - [ Alessio Fabiani ] - * [ad2dc0] Update requirements.txt - * [9bb475] Update quick_install.txt - * [c2c850] Update quick_install.txt - * [0f367c] fix hierarchical Keywords management from layer metadata - - [ Simone Dalmasso ] - * [f62544] make travis run also 2.6.x - - [ pjdufour ] - * [48b9d6] tab display settings - * [a939b6] search ui improvements - - [ Simone Dalmasso ] - * [4cb8e0] fix services - * [6774ab] Updated changelog for version 2.6b1 - * [58b8ff] - Fix Services - - [ pjdufour ] - * [5018bd] minor settings refractor - - [ Simone Dalmasso ] - * [8a8ffe] keywords and regions default should be list and not tuple - - [ Ariel Núñez ] - * [9b5134] Peg django-tastypie to 0.13.1 - - [ afabiani ] - * [caab49] - Patch for better managemen of SLDs: get title or name and avoind 'None' names - - [ Simone Dalmasso ] - * [783f7c] make sure Layer delete also removes the associated Tileset (#2999) - * [49a2af] Updated changelog for version 2.6c1 - - [ Glenn Vorhes ] - * [8e2fb2] parse separated allowed hosts - - [ Jonathan Doig ] - * [bc0f38] COrrect file name to geonode.conf - - [ Sara Safavi ] - * [cfbb9e] Provide default value if request doesn't include offset to avoid TypeError - - [ Simone Dalmasso ] - * [583b3c] remove loop from layer api (remove geogig_link) and use faster owner api - * [f0cfdb] remove unused import, thanks flake8 - - [ stan4gis ] - * [010a76] fix style for linux commands... - - [ Boney Bun ] - * [1367c1] Hide Geoserver menu when not needed - - [ afabiani ] - * [ea645f] - GeoNode Stable and Updated 2.7.x Branch without RabbitMQ Notifications integrated with GeoServer 2.10.x - * [4fb7f7] - update notification deps - * [56550b] - GeoNode Stable and Updated 2.7.x Branch / NO RabbitMQ Notifications / integrated with GeoServer 2.10.x - - [ Simone Dalmasso ] - * [96dc6d] add null and blank to tkeywords in migrations - - [ Alessio Fabiani ] - * [793606] Merge pull request #3024 from boney-bun/disable_geoserver_link - * [c79e90] Merge pull request #3029 from simod/26_fixes - - [ afabiani ] - * [81a367] - Improved Backup & Restore: recognize default GS data dir on settings - * [0167fb] - Backup/Restore command: fix json import - * [6fed1b] - Improve GeoServer upload and permissions settings - * [ff6bad] - Backup/Restore command: fix json import - * [df805e] Fix for Issue #3030 - Backup/Restore command: fix json import - * [a15a85] - Fix kombu dep - * [47723c] Fix for ISSUE #2975 - Upload Layer Issue #2975 - - [ GeoSolutions ] - * [232865] - Fix for Issue #3035: Upload Layer: the check on default style is weak. Use geometry type instead - - [ afabiani ] - * [655e97] Fix for ISSUE #2975 - Upload Layer Issue #2975 - * [622bfd] - align with 2.6.x Fix PR #3037 - * [b442e8] - allow Travis to build - * [a9131c] - Migrate Base URL Mgmt Command: Allows to easily update Maps, Styles, Layers and Links - * [c9b808] - Move down map header - - [ Angelos Tzotsos ] - * [8f2d77] Updating version number to rc1 - - [ afabiani ] - * [e7ee53] - Fix flake8 issues - - [ pjdufour ] - * [e1ea9d] added name, title, and abstract to importlayers; catch errors in social signals; fixes to importlayers --overwrite - - [ Cezary Statkiewicz ] - * [aa0564] use interchangeable notifications app, new integration code with AppConfig - - [ afabiani ] - * [8181f4] - Review and fix PR : importlayers improvements #2976 - - [ Cezary Statkiewicz ] - * [e77383] style fixes - - [ Simone Dalmasso ] - * [a2a50b] Merge pull request #3048 from travislbrundage/people-group-icons-fix - - [ afabiani ] - * [3c247e] - Review and fix PR : importlayers improvements #2976 - * [f5edd5] - Review and fix PR : importlayers improvements #2976 - * [d69e23] [REV] Upgrading FontAwesome to 4.7 and packaging to be served through GeoNode #2955 - - [ Cezary Statkiewicz ] - * [80d248] use interchangeable notifications app, new integration code with AppConfig - - [ GeoSolutions ] - * [1a4595] - Management Command : fixgeofencerules - - [ Cezary Statkiewicz ] - * [bbfccb] flake8 updates #3046 - - [ frippe12573 ] - * [28d6a4] Update geosites.txt (#3009) - - [ Simone Dalmasso ] - * [3aeeab] Updated changelog for version 2.6 - - [ Goran Mekić ] - * [6ec193] Fix for #3062: Header becomes to high if username is too long - - [ Cezary Statkiewicz ] - * [5fe2f3] mark uploads as unpublished if moderaiton flag is set #3061 - - [ Goran Mekić ] - * [d8c76d] Backport header size fix from master - - [ Angelos Tzotsos ] - * [b6dfc7] Fixing the version number - - [ Cezary Statkiewicz ] - * [54a03d] flake8 checks - - [ Simone Dalmasso ] - * [f5dee8] remove ubuntu version from manual installation - - [ afabiani ] - * [b42bf0] - Jetty Runner JVM Options: More memory in order to avoid Heap OOM - - [ Cezary Statkiewicz ] - * [7d65ea] setting for multiple recipients #3070 - - [ Goran Mekić ] - * [932779] Backport header size fix from master - - [ Angelos Tzotsos ] - * [34c426] Updating URLs in resource XML metadata - - [ Julien Acroute ] - * [74063f] Update link to the mailling list (#3075) - - [ afabiani ] - * [be7ed2] - Fix for ISSUE #3077: Select2 Fields CSS broken and tolenfields not automatically expanded - * [ca2317] - Fix for ISSUE #3077: Select2 Fields CSS broken and tolenfields not automatically expanded - - [ Julien Acroute ] - * [e4a9ff] Update link to the mailling list (#3075) - - [ Goran Mekić ] - * [79c582] Backport header size fix from master - - [ Alessio Fabiani ] - * [9fb81c] Merge pull request #3071 from cezio/3070_user_messages_multiple - - [ Cezary Statkiewicz ] - * [526f07] setting for multiple recipients #3070 - - [ Alessio Fabiani ] - * [1266cd] Merge pull request #3073 from GeoNode/PAVEMENT_JETTY - * [8368cb] Merge pull request #3065 from cezio/3061_uploads_moderation - - [ Simone Dalmasso ] - * [7ed946] remove ubuntu version from manual installation - - [ Alessio Fabiani ] - * [9844f7] Merge pull request #3064 from geosolutions-it/issue_3062 - - [ GeoSolutions ] - * [ffcf54] - Fix for ISSUE #3080: Error when trying to update owner, poc or md author on metadata editor - - [ Julien Acroute ] - * [daa454] Rename virtual env folder to match .gitignore (#3082) - - [ afabiani ] - * [baf7db] - Updating django-geoexplorer version - - [ GeoSolutions ] - * [358b58] - Clean up settings - - [ afabiani ] - * [23b543] - Settings cleanup - * [7c1a95] - Fix for ISSUE #3086: When uploading a layer from the UI, the Default Style is not correctly set and thumbnail is broken - - [ 99alex ] - * [8156a1] Update index.txt (#3088) - - [ afabiani ] - * [a1b306] - Fix for ISSUE #3085: On metadata Topic Category is mandatory, but the UI does not advertise about it - * [3d4ef8] - Fix gxp Viewer in order to parse plugins - - [ Cezary Statkiewicz ] - * [0e18e1] use ACCOUNT_OPEN_SIGNUP instead of REGISTRATION_OPEN #3091 - * [4b2c76] group categories #3067 #3083 model, api views, regular views translate strings update tests for group categories - * [5ac8d3] group categories flake8 checks #3067 - * [131505] migrations squashed #3067 - - [ afabiani ] - * [5b6efa] - Get rid of annoying tkeywords null warning - * [70cd6c] - sample tile layer background - * [0628a1] - django-geoexplorer 4.0.8 - * [3ca57f] - Align local_settings.py.geoserver.sample to settings.py - - [ Goran Mekić ] - * [55558b] Fix for ISSUE #3095: Make freetext keywords readonly settable (#3096) - * [c309fd] Fix for ISSUE #3097: Display date of group creating - - [ Cezary Statkiewicz ] - * [ec1d65] do not create email twice when user signs up #3099 - - [ GeoSolutions ] - * [6c61da] - Add sample FREETEXT_KEYWORDS_READONLY on settings py - - [ allyoucanmap ] - * [038877] - update requirements.txt - - [ afabiani ] - * [0bbeba] - Minor alignments on settings.py - * [112a4e] - Few more checks on sld_url: making absolute_url method a bit more resilient - - [ Alessio Fabiani ] - * [7419b5] Flake8 constraints - - [ afabiani ] - * [93f11a] - Fix quick install doc - - [ Etienne Trimaille ] - * [ed7be5] fix build querystring with links (#3108) - - [ Ismail Sunni ] - * [d03913] Enable ascii file upload. - * [328fa4] Unit test for ascii file. - * [4cfd28] Add ASCIIs to the error message. - - [ afabiani ] - * [fe1c90] - Fix for ISSUE #3110: Django Outh2 updated version requires updated version of DJango too - - [ Sebastian Łach ] - * [3c510d] Order fixuters during backup restore - * [d9a032] Gracefully handle non-JSON response from backup REST API - * [ba45e7] Add --backup-dir argument to restore command - - [ Rizky Maulana Nugraha ] - * [c1ee39] Fix duplicate tiling (#3117) - - [ Sebastian Łach ] - * [8d097a] Optionally skip geoserver backup and restore - * [dae56a] Backup/restore allow customized settings.ini path - - [ afabiani ] - * [6c1ceb] Release 2.7.0 alpha 1 - - [ Sebastian Łach ] - * [d894b4] Allow to override geoserver settings.ini from cli - * [745e6c] Update backup/restore docs with new CLI options - * [c25f43] Find external resources to backup - - [ Goran Mekić ] - * [9a20bc] Fix for ISSUE #3124: Fix display of group activity - - [ afabiani ] - * [923fcd] - Fix for ISSUE #3110: Django Outh2 updated version requires updated version of DJango too - - [ Sara Safavi ] - * [703a5d] Bump version number for bugfix on 2.6 release - - [ Simone Dalmasso ] - * [23534c] Updated changelog for version 2.6.1 - - [ Sebastian Łach ] - * [8c6372] Generate thumbnails for various document types - - [ Rizky Maulana Nugraha ] - * [4ce539] Fix missing frontend dependencies for leaflet. (#3128) - - [ Sebastian Łach ] - * [4e1e87] Update geonode-user-messages package - - [ Goran Mekić ] - * [039b49] Fix for ISSUE #3130: Batch edit - - [ Sebastian Łach ] - * [81edb4] Add many-to-many relation for documents and resources - * [a0ed4b] Assign user to new map created from set of layers - - [ Hisham waleed karam ] - * [4993ea] Close a tag (#3139) - - [ Sebastian Łach ] - * [73a4cd] Add templatetags to contrib.api_basemaps app - - [ afabiani ] - * [dfb39c] - Fix wrong REGISTRATION OPEN flag lookup - - [ Alessio Fabiani ] - * [cb8fbe] Align requirements to "geonode_user_messages==0.1.6" - * [2930f5] Update setup.py - - [ Goran Mekić ] - * [eedc00] Fix for ISSUE #3147: Display form errors - - [ hisham waleed karam ] - * [ea80eb] call geonode-client setThumbnail if preview is react - * [3699d7] Fix KeyError - - [ afabiani ] - * [b13017] close #3109 - Fix for ISSUE #3109: BBOX calculation in maps/models.py - - [ Goran Mekić ] - * [03cf58] Fix for ISSUE #3149: trying to change the Language - * [10b5ec] Fix for ISSUE #3150: more fields to batch update (#3164) - - [ afabiani ] - * [aa9c4d] - Updating version of Django-GeoExplorer to 4.0.9 - - [ Cezary Statkiewicz ] - * [99f1d0] notifications #3170 do not fail during notifications init - - [ afabiani ] - * [7b80f7] close #3173 - Fix for Issue #3173: Geofence rules are not aligned with layer permissions - * [446c0a] Initial Data Regions bboxes - - [ hisham waleed karam ] - * [81a2ee] remove duplicated import - * [56794c] follow PEP8 Guidelines - - [ afabiani ] - * [a81d94] close #3157 - Fix for: GeoServer: GeoFence Rule Cache should be invalidated when updating a user role - - [ Cezary Statkiewicz ] - * [2f35b8] bbox_to_wkt: remove EPSG: prefix in srid if present - - [ afabiani ] - * [438517] - Make sure 'makemigrations' is called on sync in order to reflect new packaged changes also - * [1fecaa] - Send Resource Details to Metadata Forms - * [8136b8] - Send Resource Details to Metadata Forms - - [ travis ] - * [3164ac] Add persistence to cart in search - - [ afabiani ] - * [35dd53] - Display Groups Last Modified Date - * [1cdf21] - Fix grunt dependencies for PR #3180 : Add persistence to cart in search - * [fae388] - Fix session issue for PR #3180 : Add persistence to cart in search - * [75a875] fixes #3182 : Filter by Group also the resource list - * [a8c038] - Tinify doc images (pngs and jpegs) - * [5aae1b] fixes #3182 : Filter by Group also the resource list - * [95db6a] - revert full_metadata identifier to the original one - * [ef9db2] - Add also metadata author and owner to the ISO schema - * [6ca6c3] GeoNode-PyCSW should attach access_token info to URLs as per user request - - [ Tom Kralidis ] - * [bcd885] fix ISO layer name in download links to allow binding - - [ afabiani ] - * [c2e4e9] GeoNode PyCSW: filter out not authorized layers - - [ Francesco Bartoli ] - * [d0a225] Update docs with setup and installation for centos 7 - - [ afabiani ] - * [5e88f3] close #2720 : Incomplete OGC:WMS Download Link for Catalog - * [b29509] Revert "GeoNode PyCSW: filter out not authorized layers" - - [ Francesco Bartoli ] - * [3f65c9] Fix travis build with legacy environment - * [fea1e0] Port #3168 to master - - [ afabiani ] - * [6d701e] close #2720 : Incomplete OGC:WMS Download Link for Catalog - * [19abcf] assets for #2720 : Incomplete OGC:WMS Download Link for Catalog - * [d9b455] assets for #2720 : Incomplete OGC:WMS Download Link for Catalog - * [61aa40] Minor fix for Group filtering - * [fcc35d] close #3187 : Fix CSW Tests on Travis - - [ GeoSolutions ] - * [cb2bbf] - fix render preview - - [ Alessio Fabiani ] - * [744ced] - Fix Md editors Keywords - * [dacb25] - Fix Md editors Keywords on Maps and Documents also - - [ afabiani ] - * [5b8412] - Fix flake8 issues - * [9caf88] - Fix flake8 issues - * [120f35] - Improve MD Editor Wizard: better layout - * [8c8a9a] - Improve MD Editor Wizard: better layout - * [fd9256] - Improve MD Editor Wizard: better layout - * [5674d5] - Improve MD Editor Wizard: better layout - * [1768eb] - Restore lost JS/CSS files - * [b1d26c] set the maximum text length as follows: Abstract: 2000 characters (spaces included) / Supplemental information: 2000 characters (spaces included) / Data quality: 2000 characters (spaces included) / Purpose: 500 characters (spaces included) - * [eb9223] - PyCSW: Admin should be able to see all layers anyway - - [ Cezary Statkiewicz ] - * [01dec3] geonode test data use generated dates #3192 - - [ Angelos Tzotsos ] - * [f0b314] Adding command to set all layers as public both in GeoNode and GeoFence - * [1fd134] Fixed flake8 issues - - [ afabiani ] - * [913bde] - Improve Facets Filtering accordingly to ResourceBase APIs - * [f7ac20] fixes #3198 : [GeoExplorer] Strip out custom ptypes on MapSnapshots - * [976779] closes #3194 : Upgrade httlib2 - - [ Federico Godán ] - * [7149c0] Fix remote service importation error - - [ afabiani ] - * [89cefe] - Include public groups into metadata editor - - [ Alessio Fabiani ] - * [af4bc0] - Align dependencies - * [456a57] - Align dependencies - * [40105a] - Align dependencies - * [ab001d] fixes #3205 : Improve Remote Services - - [ Federico Godán ] - * [120c5b] Fixes #3204: Create Group Fail if exist Group Category with special char - - [ gpetrak ] - * [c47c72] Changes to 'GeoNode (v2.6) installation on Ubuntu' guide to match with v2.6 and Ubuntu 16.04 - - [ @tomgertin ] - * [5cdbc3] updgrading font-awesome to 4.7 (#3217) - - [ Alessio Fabiani ] - * [8b2883] - Forward port : make sure sitemap respects at least anonymous permissions, fixes #3190 #3214 - * [60f568] Forward port on master #3195: Use href instead of text for keywords_slugs. - * [f1a8cf] Forward port on master #3221: Synchronize keywords both directions now (geoserver <--> django) - * [b7b367] - fixes #3225: Create Thumbnail should not rely on GeoServer reflector - * [a7af38] Forward port on master #3221: Synchronize keywords both directions now (geoserver <--> django) - - [ Cezary Statkiewicz ] - * [ebdc75] tests update: #3192 - - [ travis ] - * [2ac5ee] Added settings variable to inform geonode-client to request map tiles cross origin - - [ Alessio Fabiani ] - * [4baa31] - fixes #3229 : Being able to send JSON response on Documents upload also - * [c69e2a] - fixes #3231 : Small bug on GeoFence methods - - [ capooti ] - * [d235f1] Fixes #3235 for dev installation - - [ gpetrak ] - * [c53deb] Update to last modifications of GeoNode (v2.6) installation on Ubuntu guide - - [ Alessio Fabiani ] - * [a84be9] - fixes #3233 : OWSLib issue communicating with GeoServer WPS - * [19d9ec] - fixes #3233 - * [bd5c20] - Issue #3227: making setup and requirements versions coherent for flake, pep8 and transifex accordingly to the issue statement (#3240) - * [63c656] - fixes #3243 : Wrong Links on Metadata Editor for Documents - * [e94bc5] - fixes #3248 : Permission ajax_lookup for Groups misleading - * [acfc4c] - fixes #3246 : Update Notification settings to the correct PINAX ones - - [ Cezary Statkiewicz ] - * [612ce7] expose list of ows services #3250 - * [395742] pep8 #3250 - - [ Alessio Fabiani ] - * [3397f5] - fixes #3246 : Update Notification settings to the correct PINAX ones - * [8fbfe2] - fixes #3248 : Permission ajax_lookup for Groups misleading - * [33935a] - fixes #3223 : search won't work anymore using hrefs, unless search is case insensitive - * [978d32] - align with master - * [635358] - Fix Thumbnail URL generation when OL visibility is off - * [840f7c] - Fix django settings imports - * [ee0818] - Fix django settings imports - * [cfefa7] - Correctly filter TopicCategories - * [08e931] - pep8 - * [1e4dc8] - fixes #3223 : search won't work anymore using hrefs, unless search is case insensitive (#3251) - * [576c38] - - fixes #3223 : search won't work anymore using hrefs, unless search ignore cases - * [d4872a] - - fixes #3223 : search won't work anymore using hrefs, unless search ignore cases - * [e6f39f] - - fixes #3223 : search won't work anymore using hrefs, unless search ignore cases - * [cced5b] - - fixes #3223 : search won't work anymore using hrefs, unless search ignore cases - * [d608cd] fixes #3223 - * [472bc9] - Fixes: Thumbnail URL generation when OL visibility is off - * [133f34] - Fix django settings imports - * [7dbfea] - Fix django settings imports - * [f7a14e] - Correctly filter TopicCategories - * [646001] - pep8 - * [b1e1ff] - Improve forms layout: signup and documents md editor - * [97b674] - Improve forms layout: signup and documents md editor - * [7cfa6f] - Display Group (if any) on Resource Info Panel (#3259) - * [30e14d] - fixes #3223 : fix keywords filtering on base_tags also - * [c140c7] - Returns referencing infos on Layers Upload - * [b3f938] - Returns referencing infos on Layers Upload - * [b5a775] bump to version 2.7.0 - - -- Alessio Fabiani Tue, 05 Sep 2017 16:43:18 +0200 - -geonode (2.6.0+thefinal0) trusty; urgency=high - - [ Alessio Fabiani ] - * [d32a09] [Backport 2.6.x] - Add support for big ZIP files and being sure the target folder is cleaned - - [ Simone Dalmasso ] - * [583b3c] remove loop from layer api (remove geogig_link) and use faster owner api - * [f0cfdb] remove unused import, thanks flake8 - * [96dc6d] add null and blank to tkeywords in migrations - - [ afabiani ] - * [ff6bad] - Backup/Restore command: fix json import - * [df805e] Fix for Issue #3030 - Backup/Restore command: fix json import - * [47723c] Fix for ISSUE #2975 - Upload Layer Issue #2975 - * [655e97] Fix for ISSUE #2975 - Upload Layer Issue #2975 - - [ Angelos Tzotsos ] - * [8f2d77] Updating version number to rc1 - - [ frippe12573 ] - * [28d6a4] Update geosites.txt (#3009) - - [ Simone Dalmasso ] - - -- Simone Dalmasso Tue, 16 May 2017 13:00:07 +0000 - -geonode (2.6.0+rc1) trusty; urgency=high - - [ Simone Dalmasso ] - * [8a8ffe] keywords and regions default should be list and not tuple - - [ Ariel Núñez ] - * [9b5134] Peg django-tastypie to 0.13.1 - - [ Simone Dalmasso ] - * [783f7c] make sure Layer delete also removes the associated Tileset (#2999) - - -- Simone Dalmasso Mon, 10 Apr 2017 13:32:40 +0000 - -geonode (2.6.0+beta1) trusty; urgency=high - - [ Simone Dalmasso ] - * [ca2e9d] update release version - * [fac9e2] add tokenfield to assets.min.js - - [ Paolo Pasquali ] - * [0bf6c6] Fiox meta-categories in home page - * [8be3dc] Remove truncatechars in Resource Base Info Panel Abstract - * [52bf90] Fix tabs in Layer Detail Page - * [fe7667] Fix togglable tabs in detail pages - * [dd6599] Fix issue #2783 Announcements getting covered by the navbar in the index page - * [8e97e4] Enlarge people card - - [ Ezequiel Gonzalez Rial ] - * [a3957d] Remove the 3D viewer tool from maps - * [13a219] Change Geoexplorer aboutUrl - - [ Paolo Pasquali ] - * [be28a3] Add style to comment section and Page Detail About section (#2931) - - [ gonrial ] - * [ff80d8] Added {{block.super}} to all {% block extra_script %} (#2939) - - [ Paolo Corti ] - * [67b774] Fixes some style stuff (#2950) - - [ Alessio Fabiani ] - * [ad2dc0] Update requirements.txt - * [9bb475] Update quick_install.txt - - [ Simone Dalmasso ] - * [f62544] make travis run also 2.6.x - * [4cb8e0] fix services - - -- Simone Dalmasso Tue, 28 Mar 2017 11:16:56 +0000 - -geonode (2.6.0+alpha1) trusty; urgency=high - - [ Simone Dalmasso ] - * [b1254a] update release version (#2894) - - [ Jeffrey Johnson ] - * [79135f] fix requirements.txt - - [ Martina ] - * [bcdc7e] Fix issue with Leaflet library - - [ Jonathan Doig ] - * [dbe2e1] Removed uploaded/layers from Apache config - - [ Martina ] - * [20a02f] LAYER_PREVIEW_LIBRARY set to geoext as default - - [ Simone Dalmasso ] - * [dedd93] deny uploaded/layers access (#2900) - - [ Alessio Fabiani ] - * [ffbcdc] Update quick_install.txt - * [472cdd] Update quick_install.txt - * [87b5fe] Update quick_install.txt - - [ Simone Dalmasso ] - * [595023] make sure metadata detail is generalised - - -- Simone Dalmasso Thu, 16 Feb 2017 08:57:50 +0000 - -geonode (2.5.15+thefinal0) trusty; urgency=high - - * [c06f6c] update release version (#2890) - * [89f74c] add some ng cloaks (#2893) - - -- Simone Dalmasso Tue, 07 Feb 2017 08:09:34 +0000 - -geonode (2.5.14+thefinal0) trusty; urgency=high - - [ Simone Dalmasso ] - * [47a095] update release version (#2882) - * [b3cecf] fix fixtures load path in package install (#2883) - * [9a3c9a] add login logout endpoint in package local settings (#2884) - - [ afabiani ] - * [3241ab] PR for Issue #2885 - Manage signals when performing BR - - [ capooti ] - * [2851b5] Renaming a variable that is confusing when using pdb - - [ Simone Dalmasso ] - - -- Simone Dalmasso Mon, 06 Feb 2017 06:38:09 +0000 - -geonode (2.5.13+thefinal0) trusty; urgency=high - - [ Alessio Fabiani ] - * [92e646] - Fix for ISSUE #2875 - GeoFence rules are not updated if GeoNode cannot reach GeoServer Public Location (#2876) - - [ Simone Dalmasso ] - * [9b74c7] updated release version (#2878) - * [75107b] move metadata_uploaded_preserve to the right migration - * [03944f] don’t use version in leaflet libraries settings (#2879) - - -- Simone Dalmasso Thu, 02 Feb 2017 07:44:53 +0000 - -geonode (2.5.12+thefinal0) trusty; urgency=high - - * [cdf75a] further fixes in oauth setup (#2874) - - -- Simone Dalmasso Wed, 01 Feb 2017 09:54:54 +0000 - -geonode (2.5.11+thefinal0) trusty; urgency=high - - [ Simone Dalmasso ] - * [c49d46] initial 24 migrations - - [ Angelos Tzotsos ] - * [3dd63b] Update release version - - [ travislbrundage ] - * [7dd2d2] Format people cards to be universal height so sorting works properly (#2856) - - [ Simone Dalmasso ] - * [a2a508] add 2.4 initial migrations and 24 to 2.6 migrations - * [c0ebd8] Delete unused LayerStyles model - - [ Nicolas Loira ] - * [d45c75] Fixed typo in title - - [ Seno @ ThinkPad ] - * [fd51d5] fix: #2858 KeyError on logout when using qgis local_settings.py sample - - [ Simone Dalmasso ] - * [eb8d8c] paver start shouldn’t call sync (#2868) - - [ Alessio Fabiani ] - * [803ab3] Npm, Bower, Grunt commands to rebuild static libraries - - [ Seno @ ThinkPad ] - * [ae51d9] fix: get rabbitmq broker url from env file - - [ Simone Dalmasso ] - * [1f77d6] add command and package logic to handle the oauth2 configuration - - -- Simone Dalmasso Wed, 01 Feb 2017 07:44:16 +0000 - -geonode (2.5.10+thefinal0) xenial; urgency=high - - [ Way Barrios ] - * [415b0b] Oauth2 GeoServer/GeoNode implementation on Docker (#2855) - - [ Angelos Tzotsos ] - - -- Angelos Tzotsos Thu, 26 Jan 2017 03:43:09 +0200 - -geonode (2.5.9+thefinal5) trusty; urgency=high - - [ Mila Frerichs ] - * [23c42e] Add new routes/views for simple edit (#2851) - - [ Paolo Corti ] - * [1c882d] Refactor create_gs_thumbnail. Now we have same method for both layer and map thumb generation (#2850) - - [ Simone Dalmasso ] - * [ffc9e0] restore the check bbox (#2852) - * [82e34f] migrate accounts first (#2853) - - -- Simone Dalmasso Wed, 25 Jan 2017 15:13:48 +0000 - -geonode (2.5.9+thefinal4) trusty; urgency=high - - [ Francesco Bartoli ] - * [5430c0] Fix conflict account migrations by merging - * [9b013b] Move modeltranslation to 0.12 - - [ Simone Dalmasso ] - * [353ffc] inherit from user manager to keep normal functions like createsuperuser (#2835) - - [ bartvde ] - * [b7a510] Updates to README for Ubuntu 16.04 - - [ Paolo Corti ] - * [56aa76] Handle errors when saving thumbnails. Fixes #2667 (#2841) - - [ Angelos Tzotsos ] - * [9d528a] setup.py review/re-order, with Ubuntu Xenial packaging in mind (#2836) - - [ Paolo Corti ] - * [2aae06] Execute signals when running updatelayers (#2840) - - [ sbsimo ] - * [007506] docs: list the "Loading OSM Data into GeoNode" module on the index page (#2844) - - [ Ariel Núñez ] - * [af0e03] [WIP] Simpler migrations (#2629) - - [ Paolo Corti ] - * [8846ff] Fixes #2652 (#2842) - - [ Tom Kralidis ] - * [cb4f23] implement ISO 19110 support for vector layers (#2810) - - [ travis ] - * [2e70b3] Added appropriate sorting icons and fixed sorting message - - [ Alessio Fabiani ] - * [b426c4] Fixes for ISSUE #2845: Metadata form requires TKeywords but not able to enter them (#2849) - - [ Paolo Corti ] - * [d90dbc] Added fix_baselayers, a command to fix the base layers for one or more maps (#2848) - - [ Simone Dalmasso ] - - -- Simone Dalmasso Tue, 24 Jan 2017 14:36:56 +0000 - -geonode (2.5.9+thefinal3) trusty; urgency=high - - * [82cfe2] update the debian wsgi script (#2834) - - -- Simone Dalmasso Tue, 17 Jan 2017 12:08:13 +0000 - -geonode (2.5.9+thefinal2) trusty; urgency=high - - * UNRELEASED - - -- Simone Dalmasso Tue, 17 Jan 2017 10:27:33 +0000 - -geonode (2.5.9+thefinal1) trusty; urgency=high - - * [56b1b4] add python-pip and postgis2.2 config, thanks to @francbartoli (#2830) - - -- Simone Dalmasso Tue, 17 Jan 2017 08:32:08 +0000 - -geonode (2.5.9+thefinal0) trusty; urgency=high - - * [a8d827] fix dialogos dependency (#2829) - - -- Simone Dalmasso Mon, 16 Jan 2017 15:09:29 +0000 - -geonode (2.5.9+dev20170116091118) trusty; urgency=high - - [ Angelos Tzotsos ] - * [85f7aa] Update release version - - [ Paolo Corti ] - * [37686a] Adds a management command and a celery task to delete orpahed files or removed documents. Fixes #2291 (#2818) - - [ travislbrundage ] - * [21a589] Fix bug with inbox thread links becoming unclickable (#2820) - - [ Paolo Corti ] - * [7a6e0c] Fixes #2824 (#2826) - * [f38440] Adds a management command and a celery task to delete orpahed thumbs. Fixes #2535 (#2825) - - [ travislbrundage ] - * [a7b5c4] Make usernames case insensitive for authentication (#2821) - - [ Simone Dalmasso ] - * [e4c71b] remove postgis version for xenial package (#2827) - * [b1ad7c] update geoserver debian changelog (#2828) - - -- Simone Dalmasso Mon, 16 Jan 2017 09:13:18 +0000 - -geonode (2.5.7+thefinal0) xenial; urgency=high - - [ etj ] - * [fcdaad] - 2670: handling keywords thesauri - - [ Angelos Tzotsos ] - * [269724] Update release version - - [ Dan "Ducky" Little ] - * [8d0899] Adding a geogig_link URL to /api/layers (#2792) - - [ Jeffrey Johnson ] - * [245237] Dont include geonode-client in INSTALLED_APPS by default - - [ Jivan Amara ] - * [3a0b82] Short-circuit Layer.save() geoserver signal processing for gpkg tile layers. (#2794) - - [ pjdufour ] - * [5636fe] named template engine - - [ sbsimo ] - * [799b17] Enrich osm-extract docs, in the tutorials section (#2799) - - [ travislbrundage ] - * [abaf3f] Do not show delete avatar button until there is a custom avatar to delete (#2798) - - [ Daniel Berry ] - * [82a04c] When `GEONODE_LOCKDOWN = True` the /api/* endpoints need to be added to `AUTH_EXEMPT_URLS`. Otherwise GeoServer will be unable to pull the ROLES. - * [9d99d6] Bumped to version that includes option of postgis backend to also add path parsing. - - [ pjdufour ] - * [d474cf] blocks on homepage for downstream projects - - [ Toni ( –anatol ) ] - * [8e6ccb] Update all_together.txt - * [cef696] Update architecture.txt - - [ travislbrundage ] - * [2f6061] Fix bug with ajax POST and page refresh on inbox page when deleting message (#2808) - * [1a7946] Do not allow an empty geogig store to exist (#2809) - - [ Ami Rahav ] - * [7c016d] Make sure we don't have duplicate access tokens for OGC request; Remove delay from send_mail so email will be sent - * [d036ee] Fix for flake8 - - [ travislbrundage ] - * [5911cc] Fix bug with missing variable definitions on checkGeogig (#2812) - - [ Alessio Fabiani ] - * [26a9b5] Fix for Issue 2815: OAuth2 verify_token api should not throw an error if user is not authenticated (#2816) - - [ Amiram Rahav ] - * [1eddcd] Send access token only to OGC_SERVER; Workaround for expired OAuth tokens being used. (#2814) - - [ Angelos Tzotsos ] - - -- Angelos Tzotsos Thu, 12 Jan 2017 17:32:28 +0200 - -geonode (2.5.6+thefinal0) xenial; urgency=high - - [ Angelos Tzotsos ] - * [9c2b9e] Update release version - - [ Francesco Bartoli ] - * [19f1b4] Make resourcebase info panel template extendable - - [ Way Barrios ] - * [449f13] Fixing syntax error: celery_app (#2697) - - [ travislbrundage ] - * [c6168b] Sync auto generate avatar sizes with avatar sizes used in templates (#2693) - - [ afabiani ] - * [740204] 2696 Part 1 - GNIP - Better management of View and Download permissions. - - [ Ariel Núñez ] - * [b8b383] Updated AUTHORS list - - [ Alessio Fabiani ] - * [b9b05e] Update views.py - - [ travislbrundage ] - * [9b1ba9] Update AUTHORS - - [ Clarence Davis ] - * [c2f54d] Enable update layers management command to accept the permissions dictionary. (#2691) - - [ Jonathan Doig ] - * [5e6eb2] Insert missing sudo to chmod commands - * [b6db18] Clean up extra ':'s (from ::: to ::) - * [8c76e0] Language cleanup, deleted duplicate section - * [a58ebd] Minor cleanup - - [ travis ] - * [1aa95e] Process inbox messages using AJAX - - [ Daniel Berry ] - * [3fb165] - removed import on INSTALLED_SCHEMES since its usage was removed on this commit (aff43c0). - limiting the version of celery to less then the 4.0 release, due to django-celery not having a release in over a year which would include the updates in master to support the new version of celery. If you currently install Geonode it will install celery 4.0, but when attempting to use any django commands an ImportError occurs due to no module named timeutils. - - [ Jivan Amara ] - * [5dd5ce] Split geoserver.helpers.set_attributes() (#2699) - - [ Jeremiah Cooper ] - * [3d6ed5] Update the object_list limit to use the value assigned to CLIENT_RESULTS_LIMIT in settings rather than a hard coded value. - - [ Sasha Hart ] - * [a0e1eb] require celery version to be at least 3.1.18 - - [ Jeremiah Cooper ] - * [db4079] Fixed a PEP8 continuation line unaligned for hanging indent warning. - - [ Daniel Berry ] - * [49be1b] The standalone TEMPLATE_* settings were deprecated in Django 1.8 (#2711) - - [ afabiani ] - * [36d4ff] PR - (Issue #2374) GNIP: GeoServer A&A Improvements - - [ Jonathan Doig ] - * [3b9ee4] Set SITEURL to server's host name - - [ Jeremiah Cooper ] - * [9a04a9] Update the limit variable to API_LIMIT_PER_PAGE. - - [ afabiani ] - * [a620c1] (Issue 2720) - Incomplete OGC:WMS Download Link for Catalog - - [ Tom Kralidis ] - * [43c073] Revert "Fix for (Issue 2720) - Incomplete OGC:WMS Download Link for Catalog" - - [ Alessandro Parma ] - * [618f06] Fix for VM Setup with Virtualbox Doc #2577 - - [ Simone Dalmasso ] - * [caa67e] authorise read detail is requesting schema - - [ Sasha Hart ] - * [001b5b] avoid flake8 E305 (expected 2 blank lines) warnings - * [63c000] remove redundant 'constraint' key - - [ Simone Dalmasso ] - * [391969] fix #2689 - * [10231d] fix #2688 - * [5bd1bf] update doc and code to use migrate instead of syncdb - - [ Jeffrey Johnson ] - * [5eb2dc] Update quickstart docs - - [ Simone Dalmasso ] - * [1219b3] fix #2434 - - [ Sara Safavi ] - * [83f977] Add optional coverage generation & integrate CI with Coveralls.io (#2731) - - [ Simone Dalmasso ] - * [8a0bca] include tree view in assets - * [b8fbb5] treeview is now in assets - - [ Sasha Hart ] - * [3879f1] minor tweaks to spelling, punctuation, grammar (#2743) - - [ Simone Dalmasso ] - * [76d6ae] initialise variable before assignment - - [ afabiani ] - * [572fc9] - Documentation with details for Issue 2715 - - [ Way Barrios ] - * [6049bd] [WIP] Fixing geoserver connection with django container (#2749) - - [ Simone Dalmasso ] - * [b559b3] load categories with paver start (#2748) - - [ travislbrundage ] - * [9d55d1] Correctly handle xml sidecar files within a zip (#2719) - - [ Sasha Hart ] - * [cb05a1] add Sasha Hart to AUTHORS per nomination Nov 30, 2016 - - [ Way Barrios ] - * [b59548] Creating authentication for geonode into geoserver container - - [ Simone Dalmasso ] - * [f7d41c] fix #2573 (#2754) - * [c49097] add geonode.mp contrib app - - [ afabiani ] - * [f3845b] PR - (Issue #2374) GNIP: GeoServer A&A Improvements - - [ Francesco Bartoli ] - * [f0da88] Organize docker env files - * [c16900] Fix missing env files - - [ Tom Kralidis ] - * [a1271d] Use isPublished field to control whether a Resource is exposed in pycsw (#2332) - - [ Sasha Hart ] - * [bb878d] Fix doc links (#2761) - - [ Ashish Acharya ] - * [de2116] Fixed a wrong link (#2766) - - [ Dan 'Ducky' Little ] - * [4b3c75] Fix for PostgreSQL/SQLite boolean type differences - - [ travislbrundage ] - * [f096a2] Do not allow upload new image on avatar change page until an image has been selected to upload (#2768) - - [ afabiani ] - * [1b46d8] - A&A initial data fixtures and pavements scripts - - [ Tom Kralidis ] - * [0bef27] fix boolean check safely (#2771) - - [ Paolo Pasquali ] - * [42c2d5] Homepage redesign (#2772) - - [ afabiani ] - * [bd81a8] - update local_settings geoserver sample - - [ sbsimo ] - * [df48c8] add instructions on how to use osm-extract (#2777) - - [ Paolo Corti ] - * [69375e] Adding django-braces and oauthlib in requirements (#2779) - * [1245ab] Added ubuntu development installation instructions in readm and quick_install documentation (#2780) - - [ Ashish Acharya ] - * [122a8d] Removed duplicate comment (#2782) - - [ Daniel Berry ] - * [f6285b] L169 limited featured dataset title to 20 characters so the the name doesnt overlap the next dataset - * [99b1ae] removal of corsheader app, since it was not working and still required a proxy server suchas nginx or apache. The reason for removal was to reduce the nonessential app dependencies. - - [ Paolo Corti ] - * [bda8b8] Fixes #2786 (#2787) - - [ Mila Frerichs ] - * [ea8272] New Viewer: React Based (Boundless SDK) (#2790) - - [ Angelos Tzotsos ] - - -- Angelos Tzotsos Thu, 29 Dec 2016 19:55:02 +0200 - -geonode (2.5.5+thefinal0) xenial; urgency=high - - [ Angelos Tzotsos ] - * [bbfd54] Updating release info and some improvements to pavement for Xenial releases - * [fe71eb] Fixing travis build - - [ Daniel Berry ] - * [4227d8] previous adjustments to the setup.py and MANIFEST.ini were not allowing certain filetypes to be packaged, loading.gif was the most noticeable. - - [ Angelos Tzotsos ] - - -- Angelos Tzotsos Wed, 26 Oct 2016 19:40:03 +0300 - -geonode (2.5.4+thefinal0) xenial; urgency=high - - [ capooti ] - * [e672c6] Set max_length=255 for doc_file and doc_url in document model as per #issue-2285 - - [ Tom Kralidis ] - * [06ae28] add support for preserving XML metadata with upload (#2250) - - [ Simone Dalmasso ] - * [c45a75] make dynamic to work with datastore - * [b12b4d] Not make Point a multipoint - - [ Tom Kralidis ] - * [c27891] implement ident/status JSON (#2100) - - [ Francesco Bartoli ] - * [95f6a6] Fix issue #2368 - - [ afabiani ] - * [4c4195] - Issue #2337 - - [ Simone Dalmasso ] - * [287dbf] use WCS version 2.0.1, no need to do getCapabilities - * [85ddd1] update integration tests to reflect the correct mime - - [ Craig Stephenson ] - * [54e3ec] Added management command script to change titles of layers on particular maps. - - [ afabiani ] - * [9b9795] - a bit more robust check on db connection - * [538f7e] - more robust with maps without layers or broken configuration - * [06bb8c] - fix wMS test case - * [1f8c8f] - fix WMS test case - - [ Jeffrey Johnson ] - * [9b564b] Initial work on hierarchical keywords - * [3a7edd] Use a custom tag manager to handle for inserting to the tree - * [9cccf3] Adapt the api to work with the h keywords - * [1a4f27] minor fix to h keywords - - [ Simone Dalmasso ] - * [26e04e] make autocomplete to work with Hierarchical keywords - * [ae0c86] make nullable metadata_uploaded_preserve - * [9f1c82] revert previous commit, sorry - - [ Francesco Bartoli ] - * [0fbe49] Add SSL configuration to docs - - [ kamalhg ] - * [3e24eb] updating the condition in line 321 - - [ Tom Kralidis ] - * [c853c9] remove unsupported ISO property (#2385) - - [ madi ] - * [9e2a5b] added a tutorial for installing geonode devmode on centos 7 - * [0ee4ab] specified centos version - * [d42c06] added devmode install on centos to the index - * [d39a23] added devinstall centos to the index - * [ce5b5e] modified in order to render correctly in the html page - - [ Pedro Dias ] - * [3c01ac] Keywords not restricted by type - - [ madi ] - * [8ac2a1] added .. code-block:: console - * [e1f76a] added newline for readability - - [ Jeffrey Johnson ] - * [436a00] further work on hierarchical keywords - * [3b5716] fix gruntfile.js syntax error - * [004855] use load_h_keywords in search.js - * [09dddc] Move h keywords filter to snippet - * [28cb22] In progress work on UI in metadata form for hierarchical keywords - - [ Craig Stephenson ] - * [e1b42b] Added option to provide a custom date to importlayers management command. - - [ Simone Dalmasso ] - * [24f98f] fix bower package name and static imports - * [4ed2d9] add select/deselect facilities for h_keywords - - [ Jeffrey Johnson ] - * [048d05] in progress work on metadata form saving keywords - - [ Craig Stephenson ] - * [329fc4] Explicitly parse a provided importlayers date string into a datetime object. - - [ Simone Dalmasso ] - * [dce52e] is tree view.min - * [165f10] make keywords to use tags - - [ GeospatialPython.com ] - * [7c7da7] Update users-groups.txt - - [ Guillaume SUEUR ] - * [9e3619] Update django.po - - [ Tyler Garner ] - * [4e8989] Fix bug preventing set_permissions from granting layer perms to AnonymousUsers. - * [8972aa] Allow permissions to be set in gs_slurp. - - [ Jeffrey Johnson ] - * [863b5a] In progress commits from dev machine - - [ Tom Kralidis ] - * [4d01ae] support GM03 metadata - - [ Simone Dalmasso ] - * [0d9602] make kw menu collapsible - * [d4cb53] select/deselect all children - - [ afabiani ] - * [c4e7c8] - Issue #2402 - * [a4f667] - Issue #2403 - - [ Tom Kralidis ] - * [85f9d7] update deprecated Django argument - - [ afabiani ] - * [518f54] - relax few base library dependencies - - [ Tom Kralidis ] - * [cc0cb7] add CSW code example - - [ Simone Dalmasso ] - * [d0d22a] fix the datastore for the geomanager - * [3e5604] fix the datastore for the geomanager - - [ Jeffrey Johnson ] - * [0b046d] Some minor changes to development environment setup docs - * [04cf14] Getting rid of duplicate dev mode installation instructions. - - [ afabiani ] - * [a0fe13] - Merging Indexes for issue: Getting rid of duplicate dev mode installation instructions. - - [ David Alda Fernandez de Lezea ] - * [f01ad2] Commit Message - * [5a4399] wmsStore added as countable layer - * [964f43] wmsStore added as countable layer - * [ec1b6b] wmsStore added as countable layer - - [ Simone Dalmasso ] - * [c858a0] remove GeoNode.js - * [8b3f87] bind shapely to 1.3.1 - - [ jveselka ] - * [657340] Modal user menu Help link fix. Changed to Django template tag. - - [ David Alda Fernandez de Lezea ] - * [2b2294] wmsStore added as countable layer - * [4d69d7] wmsStore added as countable layer - - [ afabiani ] - * [848789] - Issue #2417 - - [ Tom Kralidis ] - * [3ff5df] remove unused fields (#2422) - - [ Jeffrey Johnson ] - * [b0716f] add zlib1g-dev to Dockerfile - - [ afabiani ] - * [e3f874] - updated db port and password on win bin installer documentation - - [ Tom Kralidis ] - * [7143d5] add startIndex and count parameter bindings to OpenSearch Description - - [ Darin ] - * [9483d9] set conditional for default map crs - - [ nathanhilbert ] - * [573c51] old win-installer script clean up - - [ Simone Dalmasso ] - * [e441c2] point quick install to the stable package - - [ Luiz Vital ] - * [b95cf7] Fixes some i18n issues: - - [ Davi Custodio ] - * [2f1f45] implementation of a exclusive page that display metadata detail for layers, maps and documents - - [ Ariel Núñez ] - * [e48ce4] Fixed typo in origin year - - [ karakostis ] - * [02706e] fix #2381 - * [11dfa9] flexibly port for dev geoserver - - [ Tom Kralidis ] - * [9f8320] bump OWSLib and pycsw - - [ afabiani ] - * [0fe791] - Improvements for gnip #2211 - - [ Daniel Berry ] - * [8c4620] osgeo-incubation, Added Copyright Headers to all python files - * [b9f3fd] pep8 fix - * [cdd50a] adjusted copyright - - [ capooti ] - * [0f9c7d] Updated download link for py2exe as reported by #2459 - - [ Tom Kralidis ] - * [12d88b] catch AttributeError on metadata with no date (#2469) - - [ jice ] - * [f25b6d] syncing with tsfx - - [ Daniel Berry ] - * [9677b8] osgeo-incubation, Added Copyright Headers to all python files - - [ Simone Dalmasso ] - * [a647c5] filter categories by is_choice. fixes #2461 - - [ afabiani ] - * [fc4085] - Issue #2430 - Update to DJango 1.8.7 - - [ root ] - * [1b2ed5] - Fix Headers CopyRight - * [a39176] - Fix migration issue with DJango 1.8.7 - - [ Simone Dalmasso ] - * [d46d53] pass is_layer in the run as extra param - - [ Jeffrey Johnson ] - * [24d618] Remove references to specific VM for tutorials - - [ etj ] - * [81bf7a] Issue #2453 Add metadata export as ISO+XSL - - [ Simone Dalmasso ] - * [6bbed8] update doc for customise look and feel - - [ Angelos Tzotsos ] - * [3f7d2c] Updated license information in readme files - - [ pjdufour ] - * [991f8e] moved urls from inside pavement code into yaml file - - [ Jeffrey Johnson ] - * [1c0aa7] Update README - * [14ec94] Update README - - [ Tom Kralidis ] - * [5df687] update ISO template link gmd:name values to be actionable - - [ etj ] - * [854e7e] #2453: Add README file to metadataxsl contrib app. - - [ mikefedak ] - * [0ba323] Clarify origin of datastore identifier. - - [ etj ] - * [3a70fe] #2488: Initial db migrations - - [ Simone Dalmasso ] - * [89e8c1] explain better the redeploy site commands - - [ etj ] - * [d99771] Upgrade django-nose version for Django 1.8 compatibility (django-nose/django-nose#202) - * [17673c] Fix fixtures list for tests. - - [ Sara Safavi ] - * [ea9e5d] Use DjangoJSONEncoder when we need to handle Decimal types - - [ etj ] - * [c747a8] #2495: Add icons associated to TopicCategories - - [ Sara Safavi ] - * [b6bcb7] use absolute path to data file to avoid IOError - - [ Tom Kralidis ] - * [613252] bump pycsw to 2.0.0-alpha1 - - [ Sara Safavi ] - * [293cfe] Update dev install instructions link - * [2e682d] 'null' has no effect on a ManyToManyField; removed from model & migration - - [ travis ] - * [b9b566] Check for existing thumbs before overwriting - - [ Sara Safavi ] - * [fdfc99] Satisfy Django with OneToOneField instead of FK where unique=True - * [71afe5] Removing fake-initial auth migration in sync to avoid errors - * [334bc5] Replace deprecated 'syncdb' with 'migrate' - - [ Daniel Berry ] - * [1cb605] geonode/js/upload/cookie.js is no longer present in static direcotry. removing script reference from layer_upload.html. - * [bc5829] removed mosaic entry in basename variable of doGeoGigToggle - * [11319a] added mapbox api basemaps functionality - - [ Asif Saifuddin Auvi ] - * [b09b38] updated catalog urls to 1.8+ - * [5c681f] updated contrib/favorite urls to 1.8+ - * [b4dae6] updated contrib/geosites urls to 1.8+ - * [32920c] updated contrib/metadataxsl urls to 1.8+ - - [ Antoine ROLLAND ] - * [47c660] set 600px instead of 400 for geoext frame for layers and maps templates - - [ Sara Safavi ] - * [483249] update README with database migration step - * [f6ffb4] update tutorial's dev install steps - - [ Daniel Berry ] - * [9f1991] added additonal api basemaps functionality - - [ capooti ] - * [9adcba] Update documentation for the MAPBOX_ACCESS_TOKEN setting - - [ Daniel Berry ] - * [87439f] added name to AUTHORS - * [e495b3] updated developers documentation for basemap additions - - [ Clarence Davis Jr ] - * [a83d0a] Refactor thumbnail files to be stored in the location configured for media storage so that whether uploaded or auto-generated, the locations are consistent when the application is configured for remote storage. - * [9f999d] Adds the TIME param to allow thumbnails generated for time enabled layers to include all features. - - [ Daniel Berry ] - * [ddd212] added declaration for 256x256 sized tiles, as per recommnedation from mapbox team - * [2a437b] added readme and comments to settings.py for the additions for the api_basemaps app - - [ amefad ] - * [2e83c4] updated index.txt - - [ Antoine ROLLAND ] - * [e7b2c7] added fields to enable vertical scroll in search bar - - [ Simone Dalmasso ] - * [ac4400] compile css - - [ Clarence Davis Jr ] - * [777d89] Adds support for s3 storage and forces layer uploads to use the FileSystemStorage - - [ Francesco Bartoli ] - * [18cb95] Fix issue #2526 - - [ Daniel Berry ] - * [72f44c] Added LDAP install and configure steps in Admin workshop - - [ Francesco Bartoli ] - * [5cb0b2] Add a name to AUTHORS - * [aac353] Fill empty value for DATASTORE variable in docs - - [ Artists Team - Cirad ] - * [154d25] Added an autocomplete template and additions to enable search by catalogue - * [023419] Created template and edited files to enable .txt and .html formats export for metadata - - [ Daniel Berry ] - * [6cb8ba] Fixes admin interface for layers - * [bb46d0] dj_database_url formats the port as an integer, whereas gsconfig is wanting a string for creating the feature store. - - [ Sara Safavi ] - * [a55949] addition to previous patch: format port as str - - [ Simone Dalmasso ] - * [2489ac] bump some packages version - - [ afabiani ] - * [25c5e2] - Restoring missing parts of the docs - * [e5e270] - update ext pakages versions - - [ Daniel Berry ] - * [be2f33] Lock in version of gsconfig - * [158e61] gsconfig now utilizes an attribution dictionary and bumped to newest version - - [ afabiani ] - * [420e82] - update geonode exts version in order to fix pavement sync db - - [ Germán Larraín ] - * [4cd274] README: fix syntax error - - [ lukerees ] - * [98a924] ol3-preview - * [1304fd] keep geoext as default - * [ce12f7] Missing quotation mark - * [0a8531] Add support for EPSG:4326 - - [ Simone Dalmasso ] - * [658c66] bump user-messages - * [c0194b] don't assume 'geoserver' in style url management - - [ lukerees ] - * [9bb138] tiles_url should be a XYZ source - - [ Daniel Berry ] - * [6dd32a] set_metadata returns four variables, which was causing an error of too many values to unpack - - [ Sara Safavi ] - * [cc73d9] 'name' no longer a ContentType field in Django 1.8+ - - [ travis ] - * [4e8e5a] Added handling of postgres backed geogig stores - - [ Darin ] - * [1bf3be] fix layer style dropdown values - - [ travis ] - * [b161a6] Default to public schema if not defined - - [ Daniel Berry ] - * [b5ae44] added exception handler for when multiple legends are returned - - [ Simone Dalmasso ] - * [35ada4] just update the tiles link url if needed, this will respect any changes to the url made by other apps like contrib.mp - - [ Sara Safavi ] - * [c0550b] register models for actstream 0.5+ / Django 1.8+ - - [ Simone Dalmasso ] - * [fab91a] re enable publish as layer group - - [ Daniel Berry ] - * [038607] adjusted text and html metadata links in the templates for documents and layers - - [ Cristian Zamar ] - * [c4f124] Changes in manual installation docs (#2561) - - [ Simone Dalmasso ] - * [22f2fa] fix layer group endpoint - - [ Sara Safavi ] - * [25445d] fix for port changes: can't concatenate str & int - * [404b57] check if layerfile_set exists - - [ travis ] - * [cfbafa] Refactor to align with updated PG Geogig plugin: calls the plugin only to create the geogig repo, and calls gsconfig to create the datastore - - [ Clarence Davis Jr ] - * [303827] fix client/server handling for multi-file upload/import by storing the upload id in the session - - [ Sasha Hart ] - * [123b3a] use .get explicitly instead of .all().filter(...)[0] - * [3b6781] use elif with mutually exclusive comparisons - * [c24de5] Invalidate layer in GeoWebCache after style_update PUT/POST - - [ Sara Safavi ] - * [459bc9] send content-type:text/xml header - * [1e9e1f] use str.join() properly - * [c54f25] fix status code check - * [4012b3] update url for cache refresh - - [ Clarence Davis Jr ] - * [c83763] prevent auto redirect when additional information is required. - - [ Sara Safavi ] - * [d6466e] bump gsconfig to 1.0.6 - - [ afabiani ] - * [cc104a] - Paver setup_geoserver using the new 2.9.x - * [ea2b29] - remove layer_is FK constraints from layer_styles migrations - - [ travis ] - * [75b9d7] Added documentation for configuring an AWS S3 Bucket with GeoNode - - [ Alessio Fabiani ] - * [3b1cd7] - test travis script with Java 8 (#2584) - - [ eg-novelt ] - * [5dd773] Adding instructions to add windows dependencies - * [73d81d] Adding gdal environment variables - * [ca7e67] Update README - * [1ace93] Update README - * [32e3c7] Update README - - [ Sara Safavi ] - * [ea2302] Revert " - remove layer_is FK constraints from layer_styles migrations" - - [ Sasha Hart ] - * [38f902] fix broken og:image link in meta tag in head - * [a84f16] fix exception during renders of admin forms using autocomplete_light - - [ Darin ] - * [6617e0] change LayerStyles related name to snake_case - - [ Jeffrey Johnson ] - * [3939a2] Create new CONTRIBUTING file - - [ Sara Safavi ] - * [eec600] restructure get_context_resourcetype for clarity - * [1628e6] add profiles & groups autocomplete config - * [83bf9a] Groups have titles, not names - - [ Ariel Núñez ] - * [8791e4] Docker for development (#2593) - * [69e5b3] Use geonode/django in celery too (#2594) - * [1300d0] make test works in docker (#2595) - - [ Sara Safavi ] - * [146eeb] Add context-specific search to /groups page - * [9a2ddc] groups do not have date or popular_count fields - * [3452f0] let custom search_content template work for both profiles & groups - * [37ad5f] Custom sort filters depending on data type - * [5bbc5a] Add context-specific search (filtering by username) within /people - * [90f176] if searching from /people context, use username instead of title - - [ Jeffrey Johnson ] - * [fdd431] url fix in README - - [ Sara Safavi ] - * [7d33db] add name to AUTHORS - - [ Ariel Núñez ] - * [a3f268] geonode-user-accounts 1.0.13 (#2603) - - [ Daniel Berry ] - * [edb11a] Fix for issue 2599. The issue was that apache was allowing directory browsing. Added the options directive to remove it from the current list of options enforced for that directory. - - [ Ariel Núñez ] - * [9942a6] Use environment variables in settings.py (#2596) - * [ce44b4] Missing migrations (#2604) - * [8d5a83] Updated readme for docker (#2607) - - [ Jorge Martinez ] - * [7c7442] Using docker images from hub - - [ afabiani ] - * [7f9d37] - (issue #2070) Layer name cannot be a style name already on Geoserver - * [781fc7] - (issue #2075) Add lxml back into paver win_install_deps - - [ Nicolas Dufrane ] - * [f59fc0] adding missing trans for #1769 - - [ julien collaer ] - * [adf68d] sycing tsf - - [ Simone Dalmasso ] - * [dd0ff0] use layer instance to revoke layer permissions, fixes #2531 (#2614) - * [874041] include data files in downstream installations should fix #2452 (#2615) - - [ Ismail Sunni ] - * [fc60a8] - Modify some settings and code to make geonode able to use QGIS Server as backend. - Add some views, templates and resources to make geonode able to use QGIS Server as backend. - Add sample setting to use QGIS Server backend. - - [ Simone Dalmasso ] - * [28ae92] should fix #2464 (#2616) - - [ pjdufour ] - * [b4ef9b] Fixed migrations, settings.py, and pavement for development - * [528c8a] pavement: moved migrate_apps to yaml - - [ Simone Dalmasso ] - * [7f31e7] fix #1718 - * [4ed20f] make debug default to true - - [ julien collaer ] - * [63f885] sycing for #1769, after #2612 merge - - [ Simone Dalmasso ] - * [10c051] fix upload issue, check if the js property exists first - * [0bbfc8] hierarchical tags support - * [7733aa] fix indentation - * [77f99a] add freeboard to requirements.txt - * [3355bd] bump taggit - - [ Ariel Núñez ] - * [628959] Don't pin versions in setup.py use >= or <= - - [ Ariel Nuñez ] - * [c24770] Pin versions in requirements.txt - - [ Daniel Berry ] - * [894ec1] Set to 2.5 final version for release - - [ Simone Dalmasso ] - * [a82f5e] update keywords integration test - - [ Jeffrey Johnson ] - * [3aec8a] fix filename - * [3efc3b] we dont use submodules anymore - - [ amefad ] - * [a366b8] Updated instruction for configure ssl on geonode: with version 2.4 some path are changed (#2571) - - [ Ariel Nuñez ] - * [f09453] 2.5.0 is out, move master branch to 2.5.1-dev - - [ Ariel Núñez ] - * [45ee6e] Delete fig.yml - * [e76d5a] Do not flake8 migrations - * [e8cacd] python-properties is now usually pre-installed in Ubuntu - - [ Christian Mayer ] - * [356758] Fix typos in installation dev docs - - [ Erik Merkle ] - * [1d0ae8] Fix GeoGig stats/logs URL for Layer info page. (#2633) - - [ Ariel Núñez ] - * [0b2797] Do not pin versions on setup.py - - [ Ariel Nuñez ] - * [16bd03] Upgrade django mptt - * [aff43c] Use a pattern for adding package data, fixes missing fixtures on packages - * [626990] Set development version for 2.5.3 - - [ Francesco Bartoli ] - * [5e4e5d] Add volume data container to geoserver - - [ Craig Stephenson ] - * [6609a4] Exclude remote services layers from map download feature. - - [ travislbrundage ] - * [287a84] Make download links open in new tab (#2642) - - [ Erik Merkle ] - * [6bc231] Fix race condition during Raster upload/import - - [ Francesco Bartoli ] - * [6b24bf] Add variables for building base_url - - [ Ariel Núñez ] - * [545564] local_settings.py should be called only once - - [ Way Barrios ] - * [d588d5] Provide defaults for optional ogc_server_settings (#2647) - - [ Ariel Nuñez ] - * [6cf89e] Move master to 2.5.4 - - [ travis ] - * [b3068d] Minor text fix for profile update - - [ Etienne Trimaille ] - * [9b6329] fix QGIS path log when using qgis server backend (#2648) - - [ travis ] - * [1f6578] Bugfix for updating style through UI - * [0cfd4d] Clear comment modal after adding comment - - [ Alessio Fabiani ] - * [f5f89b] Issue #2401: GNIP: GeoNode Backup and Restore (#2651) - - [ travislbrundage ] - * [2856aa] Respond correctly when user removes the last related file for a layer upload (#2660) - - [ Simone Dalmasso ] - * [08dd00] bump guardian and manage the anonymous pk internally - * [0c78f3] use names instead of slugs for keywords - - [ Daniel Berry ] - * [cf14e3] ini file was not an allowed file type for MANIFEST.in. This was causing the settings.ini file to not be included in the package. - - [ travis ] - * [12c89c] Fix small bug where {{ repoUrl }} was being used as geogig email due to incorrect input type in HTML - * [102584] Fixing missing and broken links to actions on the user's activity page - - [ Angelos Tzotsos ] - * [fc5c4e] Updating pycsw to the latest released version 2.0.2 - - [ Tom Kralidis ] - * [503250] bump pycsw - - [ Alessio Fabiani ] - * [6c7115] Update conf.py - * [d12d16] Update setup.py - * [697398] Update conf.py - * [946359] Update conf.py - * [203cbb] Update requirements.txt - * [1c6446] Update conf.py - - [ afabiani ] - * [0fd256] - Fix the docs - - [ travislbrundage ] - * [a6dd4c] Updating Choose Files button to allow user to reupload the same file again without having to use Clear (#2681) - * [89224c] Use new UserActivity view (#2680) - - [ Sasha Hart ] - * [0ebe41] truncate upload names over length to allow upload (#2671) - * [03e242] remove mutable default values for kwargs (#2668) - - [ Jeffrey Johnson ] - * [f684cb] first of many docs fixes (#2684) - - [ travis ] - * [854fcf] Fix the Resources section on profile detail page to work when no data exists in the category yet - - [ Angelos Tzotsos ] - * [99aca2] Incubation documentation fixes (#2687) - - [ travis ] - * [962504] Fixed bug breaking Group Activity view - - [ Angelos Tzotsos ] - - -- Angelos Tzotsos Tue, 25 Oct 2016 22:17:23 +0300 - -geonode (2.4.0+thefinal0) trusty; urgency=high - - [ Tom Kralidis ] - * [37c3d7] support XML metadata uploads with filenames like xyz.shp.xml (#2351) - - [ Simone Dalmasso ] - * [9af583] hide the add to cart icon if there's no cart - * [7c1932] the add to cart is now visible despite the user is logged in or not - - [ Paolo Pasquali ] - * [13349d] Minor layout fixes in People and Group pages - - [ Simone Dalmasso ] - * [388624] don't show the resource div if there are no resources in profile detail - * [9ec815] limit title length in cart - * [053135] Change cart default text - * [fccfd2] updated few screenshots - - [ Paolo Pasquali ] - * [de61c5] Documents Upload Page improvements - - [ Simone Dalmasso ] - * [8a91d1] test doc build without readme - - [ ilpise ] - * [a98a4b] Mobile Design / Extent Search #2171 - * [9bca62] fixed logs - * [921084] changed map initial zoom to 0 - - [ Pedro Dias ] - * [642f19] Fixed the problem with DateTimeFields not accepting PM datetimes - - [ ilpise ] - * [ee9320] Changed $scope to var - - [ Pedro Dias ] - * [dc011d] Corrected flake8 violations - - [ Simone Dalmasso ] - * [6f9ed4] bump owslib to 0.10 - - [ Ariel Nunez ] - * [17b343] Added instructions to build geoserver debian package - - [ Tom Kralidis ] - * [04ddf1] make CSW GetDomain display facet counts - - [ Ariel Nunez ] - * [2716c3] Updated geoserver's debian package building documentation - * [09f892] Updated geoserver's debian package building documentation - - -- Ariel Nunez Thu, 19 Nov 2015 14:48:54 -0500 - -geonode (2.4.0+rc4) trusty; urgency=high - - [ x ] - * UNRELEASED - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 18 Nov 2015 15:48:03 -0500 - -geonode (2.4.0+rc3) trusty; urgency=high - - [ Simone Dalmasso ] - * [39f179] initial support for shopping cart - * [69fbd5] add bulk permissions in cart - * [bfcafa] removed unnecessary css rule - * [696234] fix cart empty message - - [ gannebamm ] - * [176eb2] added german as language - - [ Simone Dalmasso ] - * [5c4005] fix dynamic to correctly use the basefile - - [ julien collaer ] - * [07cd09] sycing - * [fd80e8] sycing tsf - - [ Simone Dalmasso ] - * [0b2ac3] add verbose name plural and set geomanager as default for dynamic - - [ pjdufour ] - * [eca65b] Message posted to Slack after layer upload - - [ Gavin Reich ] - * [0b75df] get doc_url extension using urlparse - - [ Francesco Bartoli ] - * [357bec] Clear all SEVERE and ERROR messages from building docs - - [ Paolo Pasquali ] - * [caad6f] Update - * [e5775c] Fix People page layout - - [ Simone Dalmasso ] - * [29ca8c] Revert category choice in upload, also fixes #2323 - - [ Francesco Bartoli ] - * [f5f6b8] fix more inconsistencies on docs source files - - [ Simone Dalmasso ] - * [e6ceb5] set additional store parameters, fixes #2337. Thanks @afabiani - - [ julien collaer ] - * [856e23] sycing - * [62d4ba] sycing tsf - - [ pjdufour ] - * [92d649] Message posted to Slack after layer upload - - [ Gavin Reich ] - * [f14b3e] get doc_url extension using urlparse - - [ Francesco Bartoli ] - * [262c52] Clear all SEVERE and ERROR messages from building docs - * [ba3ce6] fix more inconsistencies on docs source files - - [ Paolo Pasquali ] - * [4a56ac] Fix group and profile layout page - * [887a19] Fix logo in group detail page - - [ Daniel Berry ] - * [c672bc] 'added additional filetypes for geonode.importer - gdal specific' - - [ Paolo Pasquali ] - * [e1da56] Fix keyword style in group page - - [ Jude Mwenda ] - * [da22ec] Changes to include spherical projections and better error handling when logging - - [ Tom Kralidis ] - * [f604af] add support for data.json output (#1022) - - [ Paolo Pasquali ] - * [a466ac] Restore primary button in group list page - * [ecdccd] Restore settings and search.js - - [ afabiani ] - * [2e3870] - Proposal Refactoring for GeoNode Documentation - - [ Simone Dalmasso ] - * [401882] use sld_url to retrieve the correct sld location, fixes #2048 - * [1c5f7d] add cart button and add/remove behavior - * [54b034] more fixed own the cart templates - * [fe3402] add credentials on was statistics generation, fixes #1412 - - [ capooti ] - * [3be01b] Now bingmaps base layer will be enabled if a BING_API_KEY is defined. Refs #2314 - - [ Paolo Pasquali ] - * [013990] Fix cart templates - - [ Simone Dalmasso ] - * [a1b744] body_href is a property then and bump gsconfig to 1.0.3 - - [ capooti ] - * [603b4f] Fixes #2301 - - [ x ] - - -- x Wed, 18 Nov 2015 15:08:41 -0500 - -geonode (2.4.0+rc2) trusty; urgency=high - - [ Simone Dalmasso ] - * [7ed091] make sure there are upload sessions before updating the user - - [ capooti ] - * [29800b] Fixes html layout in some account pages - - [ Simone Dalmasso ] - - -- Simone Dalmasso Thu, 22 Oct 2015 13:32:21 +0000 - -geonode (2.4.0+rc1) trusty; urgency=high - - [ Simone Dalmasso ] - * [c60705] remove base layers from package local_settings. - - [ Ariel Núñez ] - * [9bd18c] Added gsolat to AUTHORS - - [ capooti ] - * [62e706] Fixes #2310 - * [0c3c13] Better handles topic category pick in layer upload form - * [71da95] In layer upload topic category must be passed as string - * [e7097e] Adding geonode version number to static files in order to force reload in browser every time there is a new geonode version - - [ Tom Kralidis ] - * [d0362d] bump pycsw / OWSLib - - [ Simone Dalmasso ] - * [896f62] fix some text search issues, special characters, positioning and free search - - [ capooti ] - * [541d8e] Fixes layout for some pages - - [ Simone Dalmasso ] - * [ab009d] add owner autocomplete in metadata page - * [3f9a7a] assign upload session to owner on metadata save, fixes #2222 - - -- Simone Dalmasso Wed, 21 Oct 2015 11:37:46 +0000 - -geonode (2.4.0+beta28) trusty; urgency=high - - [ Charles Cosse ] - * [994e60] 1. Pass category_form from views.layer_upload to layer_upload.html 2. Require user to choose category before uploading (upload.js) 3. Pass chosen category via cookie (cookie.js, upload.js) 4. Parse category_id from cookie in POST section of views.layer_upload - * [b88195] 1. Pass category_form from views.layer_upload to layer_upload.html 2. Require user to choose category before uploading (upload.js) 3. Pass chosen category via cookie (cookie.js, upload.js) 4. Parse category_id from cookie in POST section of views.layer_upload - * [cc3844] Chage "//" to "#" for .py comment - * [71bf5d] Coding style fixes based on Travis CL output - * [7e19e1] Whitespace fixes as per Travis Cl output: - * [8d6fb5] More whitespace nonsense - * [d11feb] Whitespace hell - * [ae591f] Whitespace hell 4real - * [643d64] Consistent use of whitespaces - - [ capooti ] - * [de29e6] Correctly aligned "Your selections" and "Clear all filters" button to the rest of the page - - [ Stefano Menegon ] - * [49334c] fixed issue with years before 1900 - * [a9d794] removed unused package - * [6dbce4] added comment on strftime issue - - [ Charles Cosse ] - * [206165] Added explicit import of cookie.js - - [ Tom Kralidis ] - * [d08265] add support for preserving XML metadata with upload (#2250) - - [ Jeffrey Johnson ] - * [4801a8] Revert "(Hold for 2.5.x) add support for preserving XML metadata with upload (#2250)" - - [ Charles Cosse ] - * [c2ee05] Using geonode-supplied {% static %} to accomodate non-root install - - [ Daniel Berry ] - * [dbb214] add wgs84 map viewer support - - [ afabiani ] - * [9822c1] - Adding afabiani among AUTHORS list - - [ Charles Cosse ] - * [801c03] removal of old references to form.cleaned_data["category"] which are not used (yet) because passing chosen category via cookie rather than adding category form to formset at submission. - * [c9b386] remove logging statements ... ooops. - - [ Simone Dalmasso ] - * [3ae8de] explicitly create the uploaded directories and assign them to apache - * [f2707e] exclude bing until we figure out what changed - - [ pjdufour ] - * [9891f5] More resilient layer delete - - [ Simone Dalmasso ] - * [09c0c2] fixed issues with alternative map projection - - [ Tyler Garner ] - * [04ff6d] PEP8 fixes. - * [0f31fc] Bump Django to include latest security patch. - - [ capooti ] - * [8b4e46] Includes a API_INCLUDE_REGIONS_COUNT setting to enable region facets count. It also add region code to the api filtering fields - - [ Simone Dalmasso ] - - -- Simone Dalmasso Wed, 14 Oct 2015 11:34:35 +0000 - -geonode (2.4.0+beta27) trusty; urgency=high - - [ Simone Dalmasso ] - * [355de3] add owners api, faster version of profiles for filtering purpose - - [ capooti ] - * [d2db06] Added storeType as filter in layer admin page - * [b1913a] Fixes #2269 in layers and documents metadata pages - - [ Stefano Menegon ] - * [e775a6] Case insensitive search on GeoExplorer Find Layers - - [ root ] - * [c17fa0] - Fix for Issue #2273 - - [ julien collaer ] - * [dac658] sycing with transifex and removing 5 unused translations fr_FR us_US - - [ afabiani ] - * [3a856c] - Improvements for the management of the Importer Session next ID - - [ root ] - * [1382e8] - fix pep8 violations and typos - * [d66de1] - removing celery kwargs, need more investigation - - [ Jonáš Veselka ] - * [17b6b2] Fix LOCKDOWN_GEONODE not working - - [ afabiani ] - * [fdcdff] - Fix for 2279 - - [ Sarin Kesphanich ] - * [084b50] Fix When choose Window-cp-874 encoding Use 'cp874' https://docs.python.org/2/library/codecs.html#standard-encodings - - [ capooti ] - * [c779f2] Moving the category field label to the correct place in the layer metadata form - * [32d5e1] Upgrade font awesome to 4.4.0 - * [3317a1] Fixing a string concatenation for pycsw description in settings - - [ Ariel Nunez ] - * [38e2cf] Upgrade gsconfig and gsimporter to 1.0.0 - - [ Simone Dalmasso ] - - -- Simone Dalmasso Tue, 29 Sep 2015 15:08:39 +0000 - -geonode (2.4.0+beta26) trusty; urgency=high - - [ Matt Bertrand ] - * [468086] Filter out any user permissions that are not in the PERMISSIONS_TO_FETCH list - * [d58fdf] Fix issue with permissions filter when HAYSTACK_SEARCH = True and SKIP_PERMS_FILTER = False - - [ Simone Dalmasso ] - * [e58d92] fix map_download permissions check, fixes #2145 - - [ = ] - * [39d86b] Reversing some url that were hard coded - - [ Simone Dalmasso ] - * [d49556] add back the GroupInvitation send method - * [d38e02] fix remove deb package - * [1e1e83] convert indentation to spaces - - [ Matt Bertrand ] - * [096684] Proxy for API - use appropriate search method depending on whether haystack is enabled or not - * [db6635] Revert "Proxy for API - use appropriate search method depending on whether haystack is enabled or not" - - [ = ] - * [e6a6ff] Fixes an unicode issues with a logged string - - [ Matt Bertrand ] - * [ebeac4] Make haystack search results look more similar to standard API search results - - [ Simone Dalmasso ] - * [7867e8] demo.geonode.org management script - * [b89809] fix errors and add single build - * [d7acd0] make it pep8 compliant and fix small issues - * [e9a7c3] add some inline documentation to the demo_site script - * [5f3c0d] update home page mailing list ref - * [757bd4] assign geonode/uploaded to the apache user in post installation - * [9e0947] don't translate link names - * [549c2a] update level mailing list in readme - * [85e635] add geonode support in Readme - - [ Tyler Garner ] - * [98d398] support GeoJSON upload through geonode.upload. Relies on 2.6+ fix https://github.com/geoserver/geoserver/commit/a7efe793d3b1025eceb41c899f381a5b16df5b9b - - [ Matt Bertrand ] - * [7fd2d6] Add owner filter to haystack search - - [ Alessandro Sarretta ] - * [ca97f2] Update base.txt - - [ Simone Dalmasso ] - * [e089d0] some optimizations to the api - - [ julien collaer ] - * [31f61e] latest sycing with transifex.com - - [ Simone Dalmasso ] - * [e06c89] don't use class attributes to store api filters - * [9bb1bb] fix flake8... - - [ Matt Bertrand ] - * [9fd28b] Make Haystack search indexes more consistent across layers, maps, documents - - [ Jeffrey Johnson ] - * [1348ef] Update jenkins script - * [ec8c38] more work on jenkins setup - * [6def1c] more work on jenkins - - [ John Jediny ] - * [4857c6] Fix for autocomplete for Search UI Issue #2152 - - [ Matt Bertrand ] - * [1f0bf7] Fix silent failure of cascading_delete when a PostGIS datastore is being used. - - [ Tom Kralidis ] - * [7f0be6] Update pycsw_local_mappings.py - - [ = ] - * [a49f96] Updated the mailing lists endpoints - * [b19842] Updated the mailing lists endpoints - - [ Tom Kralidis ] - * [afc6a9] hardwire dates (#2170) - - [ capooti ] - * [9b8e9c] Now people list page is paginated - * [5b9f07] Fix a potential error - - [ Simone Dalmasso ] - * [8f7d16] make set_default_permissions respect settings - - [ Matt Bertrand ] - * [c4d457] Fix Haystack search issue with regions containing spaces. - - [ pjdufour ] - * [8edcd9] Fix #2175 - - [ Cristian Zamar ] - * [3c3b50] fix encoding problem when using Spanish lang (accents) - - [ julien collaer ] - * [73025e] sycing with Transifex - - [ Stefano Menegon ] - * [40f1d7] Fixed checking for keywords - - [ Paolo Corti ] - * [41a054] Readding South Sudan to region list. Fixes #2117 - - [ capooti ] - * [9cbeca] Readding South Sudan to region list. Fixes #2117 - - [ Tom Kralidis ] - * [8a9a87] update gmd:dateStamp on metadata save (2187) - - [ Matt Bertrand ] - * [bb3afc] Make lazyloading code consistent between layer detail and new map views - - [ Stefano Menegon ] - * [2944e0] allow admin user to edit other users profiles - - [ pjdufour ] - * [62d63d] incorporated suggestions - * [1b59c4] Translation fixes - * [87cd42] Added SRID Flag - * [4b5b23] Initial twitter work - - [ capooti ] - * [411394] Handling Wand fails. Refs #2194 - - [ Tom Kralidis ] - * [9dd12b] fix keyword_csv (https://github.com/geopython/pycsw/issues/339) - * [0e9d73] update ordering in csv test - * [197049] fix flake8 - - [ Ariel Núñez ] - * [51a42a] Updated instructions for static development - * [e1cfbf] Added more grunt modules to the docs. - - [ Guillermo Solano ] - * [f9dbd9] Fixed paver static task and removed unneeded Makefiles - - [ pjdufour ] - * [de3be8] OpenGraph meta - - [ Simone Dalmasso ] - * [67a48b] use binary in save thumbnail - - [ Guillermo Solano ] - * [0f4409] Only grunt production generates the correct CSS - - [ Ariel Nunez ] - * [da3573] Updated all assets after running grunt production - - [ Matthew Hanson ] - * [c2cb55] import geonode.celery_app instead of celery_app as it fails in production - - [ pjdufour ] - * [02e405] Refractored - - [ capooti ] - * [13bd98] agon rating can be blank in admin - - [ Stefano Menegon ] - * [8f91a6] added filter by owner and SEARCH_FILTERS configuration - - [ etj ] - * [7627e5] Fix log message in upload.js. Closes #2199. - - [ Stefano Menegon ] - * [392d09] reused ProfileResource - - [ Tom Kralidis ] - * [289843] update pycsw dependency - - [ pjdufour ] - * [da4f63] Added twittercard and opengraph support to profiles and groups - * [d78d3a] added to docs - - [ Patrick Dufour ] - * [1cd2b1] Added pjdufour to AUTHORS - - [ pjdufour ] - * [49265e] Added regions and tarfile support to importlayers - - [ Tyler Garner ] - * [00a2fb] Fix JS error that causes the upload to forward the user to /upload/undefined after configuring time. - - [ pjdufour ] - * [2e1856] initial schema.org implementation - - [ Simone Dalmasso ] - * [d78469] fix wrong apache ownership assignment - - [ pjdufour ] - * [a19092] Fix #2209 - - [ Simone Dalmasso ] - * [525c0e] make sure the thumbnails links always use forward slashes - - [ Clarence Davis Jr ] - * [31e38d] port Favorites from MapStory to GeoNode contrib module. see readme.txt for more info. - - [ Tom Kralidis ] - * [78c14d] add OpenSearch and OAI links [ci skip] - - [ etj ] - * [892c69] Fix for upload of zipped shapefiles. Closes #2220. - - [ pjdufour ] - * [35a299] EXIF Support - - [ Tom Kralidis ] - * [9bce33] comment out unused values for default CSW - - [ capooti ] - * [a663ed] Uploadsession can be empty under certain circomstancies, ie layer cascade delete when removing an user - - [ pjdufour ] - * [bd3a64] Initial Slack integration via contrib app - - [ Simone Dalmasso ] - * [740af8] added geosites as contrib module - - [ nathanhilbert ] - * [9a32f3] expose geoserver in docker - - [ Matthew Hanson ] - * [45cca1] geosites settings updates - - [ pjdufour ] - * [9ab7e1] Initial NLP work - - [ Matthew Hanson ] - * [9f40f7] removed old code from geosites site_template - * [0aff80] updated GeoSites documentation - - [ Simone Dalmasso ] - * [9f963a] no flake 8 on gusset local settings - * [08ef6e] update demo site ip in jenkins script - * [119f7f] added resources and users management to the geosites doc - - [ julien collaer ] - * [3c367c] latest sycing with transifex.com - - [ Jeffrey Johnson ] - * [4baca0] dont enable contrib apps by default - - [ Matthew Hanson ] - * [0a3e2e] updated GeoSites README - - [ Tom Kralidis ] - * [659a43] implement OpenSearch autodiscovery - * [1bd369] implement OpenSearch autodiscovery - - [ pjdufour ] - * [58b5ff] fix to exif - * [0a1b3c] Added NLP hook for layer upload - - [ Patrick Dufour ] - * [7441e5] Added README to Slack Contrib App - - [ Tom Kralidis ] - * [4f7ec9] fix flake8 errors - - [ Patrick Dufour ] - * [156e20] Add README to NLP Contrib App - - [ Matthew Hanson ] - * [a3da40] geosites settings adjusted - - [ Simone Dalmasso ] - * [e73078] fix flake8 - - [ pjdufour ] - * [1d6969] Extract regions from ISO metadata - * [38de64] Fix #2166 - * [6f6d9b] minor ui fix for announcements - - [ Tom Kralidis ] - * [70cb5f] add region support for FGDC and Dublin Core uploads (#2237) - * [e62a82] fix ref - * [0b8f83] s/PyCSW/pycsw/ - - [ Matthew Hanson ] - * [fda8d1] geosites, properly adjust gs location in post_settings - * [dd4fa5] geosites - add GS url and project name to auto-generated site directory - * [30909f] fixed geosites reading settings template - * [624573] geosites readme update - - [ etj ] - * [b28ead] Cleanup fix for #2220. - - [ Tyler Garner ] - * [93a21d] Profile index can directly access user methods. - - [ Ariel Núñez ] - * [6732b9] Added missing import - - [ Simone Dalmasso ] - * [30c6e3] fix indentation to spaces - - [ Ariel Nunez ] - * [920d41] Switch to black and white geoexplorer - - [ jwood ] - * [3d9345] doc only change - update readme file. - - [ Maungu Oware ] - * [8afd5f] added hyperlinks to downstream projects - - [ Matthew Hanson ] - * [8a0280] fix template and static folder paths - - [ Patrick Dufour ] - * [a478df] Create README.md - * [859da4] update to exif readme - - [ Ariel Núñez ] - * [80c220] Update changelog - - [ Simone Dalmasso ] - * [6e455d] update the permissions layer instead of loop over permissions, fixes #2247 - - [ Ariel Núñez ] - * [c01c6f] Added missing PROJECT_ROOT in local_settings.py - - [ Craig Stephenson ] - * [0d33bd] Validate remote service name to comply with GeoServer workspace name restrictions. - - [ Simone Dalmasso ] - * [950ca9] add missing ul - - [ Matthew Hanson ] - * [9ed64f] remove resetting of geoserver url - * [93cb6e] WCS should use internal geoserver location not public url - - [ Dimitri Justeau ] - * [e40d4a] [doc] Only one datastore name in custom_install - - [ Syrus Mesdaghi ] - * [d242c3] fixes issue 2254 where any file type that has and ext longer than 3 characters cannot be uploaded into a geogig repo - - [ Simone Dalmasso ] - * [845cee] fix api detail url for non resourcebase - - [ Craig Stephenson ] - * [41dfbe] Use workspace name in namespace URI for WMS services to avoid collisions. - * [33b75f] Delete remote services from GeoServer when they are deleted from GeoNode. - * [16313e] Made indentations multiples of four to fix PEP8 violations. - - [ Simone Dalmasso ] - * [fb1fa1] support the 102113, old esri definition for 3857 - - [ GistdaDev ] - * [948a28] Change language name - - [ afabiani ] - * [05195d] - Issue #2258 - - [ Simone Dalmasso ] - - -- Simone Dalmasso Mon, 14 Sep 2015 06:37:52 +0000 - -geonode (2.4.0+beta25) trusty; urgency=high - - [ Nicolas Dufrane ] - * [235d80] Fix issue 2022, layer searcher is back - * [6f9267] fix typo - - [ Matt Bertrand ] - * [b8cd4f] Add options to specify layer category and restricted view permissions - - [ capooti ] - * [1bcdf7] Truncates category description in categories filter. Otherwise the menu is messed up from long category names - - [ Simone Dalmasso ] - * [623f05] check whether the ip has changed in update layers and fix the metadata links - * [fe2a73] don't load announcements tags in 500.html - * [90344b] check both for boolean and string as gsconfig can return either one or the other. (will be cleaned out when gs is fixed). fixes #1889 - * [48c1de] listen for pagination on the outer element - - [ Matt Bertrand ] - * [ea2d49] Add option for specifying title - * [35aa6d] Get rid of return line - * [90ff92] Formatting - * [b8bec9] use self.stdout; fix newline - * [5cc937] Update commands.txt - * [73027a] Update commands.txt - * [366fd8] Update options for importlayers command - - [ Simone Dalmasso ] - * [56406c] add missing closing paragraph - * [71ac63] don't apply the map height to all leaflet maps - * [fbff59] restrict the spatial filter listener to the filter map - - [ Matt Bertrand ] - * [255cb1] Fix error with keyword filtering when settings.SKIP_PERMS_FILTER=True - - [ TeamGeode ] - * [37e3ed] add and custome announcement_confirm_delete.html - - [ Simone Dalmasso ] - * [4d96ee] update template 500 - * [875949] Delete object permissions on resource delete and simplify some queries - * [db452c] make flake8 happy - * [c906a6] forgot ContentType - * [f22948] rewrite the guardian get_users_with_perms - * [dac4f8] rename to remove_object_permissions - - [ capooti ] - * [7d8599] Now the keywords and regions in resource info panel are linkified with the api search string - * [997f8d] Added a script to generate a full GeoNode instance to test performance with - - [ Simone Dalmasso ] - * [e8b123] do post process counts instead of per resource count - * [3e2f50] remove resource base from admin - * [183dbd] optimize some queries - * [a91b1d] fix typo - * [116aeb] make better use of gsconfig to avoid too many geoserver calls - * [ff0611] fix permissions management - * [cf0849] no need to call remove_objet_permissions twice anymore - * [20aa44] improve layer_acs performances - * [e1d92a] fix indentation - - [ Ian Schneider ] - * [6752f9] add missing libraries for DEBUG_STATIC mode - - [ Simone Dalmasso ] - * [451c98] call gsconfig with the store in cascading delete - * [67bf0c] fix the no_custom_permissions check - - [ TeamGeode ] - * [df1bfd] improve visual of document_detail page - - [ Simone Dalmasso ] - * [c82a82] optimize resources faceting - * [5a9f00] use values instead of values_list, thanks @ischneider - * [c6ca25] add direct link to map view in map snippet - - [ TeamGeode ] - * [86479b] add responsive tag - - [ Simone Dalmasso ] - * [edc374] override the gs_config get_store to avoid request of all the stores - * [2d4b55] reuse the gs_resource between signals if available - * [b4db40] reuse the workspace in geoserver_upload - * [567c4b] fix error and better use of workspace - * [75ad7e] add the featured name to featured maps urls - - [ Matt Bertrand ] - * [a7e295] Fix issues with Haystack search (date, regions) - - [ Simone Dalmasso ] - - -- Simone Dalmasso Mon, 04 May 2015 14:12:34 +0000 - -geonode (2.4.0+beta24) trusty; urgency=high - - * [30dc9e] add back the geonode.zip install step - - -- Simone Dalmasso Tue, 07 Apr 2015 09:14:17 +0000 - -geonode (2.4.0+beta23) trusty; urgency=high - - [ Allan Oware ] - * [92c13c] allowed document types list - * [caf46e] replaced numbered list with bullet points - - [ Simone Dalmasso ] - * [c6dca9] calculate map extent based on a standard screen resolution - * [f78fb1] update comments and add an assertion in map tests - - [ Geode Team ] - * [749872] Correct proxy fail for specifics characters - - [ Allan Oware ] - * [1ffa10] passed array as context parameter in view - - [ Simone Dalmasso ] - * [4eceea] use full url in thumbnail link generation, fixes #2074 - * [603ce6] don't save document twice on metadata save - * [7c1d0b] don't consider the port in the link sanitizing loop (preserve the thumbnail remote link) - * [f1962f] bump gsconfig, fixes #2050 - * [fdbd27] better sniff whether a layer is remote to not execute the signals - * [664e0b] DRY metadata forms - * [ea1d9d] fix new flake8 complains - * [31cda0] specify pep8 version to avoid continuous updates - * [d3d069] fix notification import for now - - [ Geode Team ] - * [0768a0] Checking and editing column names for layer upload - - [ Simone Dalmasso ] - * [4c9c66] add autocomplete for keywords, fixes #1325 - * [170844] sorry flake8 - * [f2b6c8] add o/c server log_file also in local settings sample - - [ Geode Team ] - * [c04cb3] add awesome-slugify on pip install - * [6387dc] some correction for Travis - * [232835] flake8 correction - - [ Tom Kralidis ] - * [561ece] fix ref (#2082) - - [ GeodeTeam ] - * [0f9f04] remove print qry - - [ Simone Dalmasso ] - * [c757ab] add awesome-slugify to deep packages and use it for groups name, fixes #2073 - * [4e7d02] add autocomplete keywords in admin - * [e6bc8b] update the ubuntu_config_file to do more tasks and update docs - * [74b226] Add zip upload - - [ julien collaer ] - * [1c5362] sycing with transifex month 2-2015 - - [ Simone Dalmasso ] - * [098c2f] fix permissions form js to reflect the default permissions - - [ CORTI Paolo ] - * [0773d8] Notifications urls enabled only when necessary - - [ capooti ] - * [558674] Notifications urls enabled only when necessary - * [836bc2] Sorry flake8! - - [ Simone Dalmasso ] - * [df225a] fix js behavior on permissions widget when the anonymous user has no permissions at all - * [1d6277] re-trigger travis build - - [ Stefano Menegon ] - * [1781dc] Fixed FieldError on admin search - - [ capooti ] - * [6599d7] Now using the internal test client instance in tests instead than instantiating the client at every test - * [5ba7a3] Now flake8 does not complain of unused imports - - [ Tom Kralidis ] - * [6b3bcb] update package descriptions - - [ TeamGeode ] - * [64def6] adding extra_user_menu into user menu - - [ Tom Kralidis ] - * [6223e5] add ability to override pycsw server settings in settings.py (#2099) - * [e7e59a] fix flake8 error - - [ TeamGeode ] - * [1587a3] Resolve malformed XMLHttpRequest from in IE11 - * [16f4f1] add has_key HTTP_USER_AGENT - * [84f11a] flake8 - - [ capooti ] - * [fbf520] Load categories, regions and keywords only if relative filters are available in the page - - [ state-hiu ] - * [395cac] Added group to homepage facets - - [ Matt Bertrand ] - * [db482d] Fix importlayers command for zip files - - [ Ian Schneider ] - * [bd4e02] fix for case-sensitive extension comparison - - [ Simone Dalmasso ] - - -- Simone Dalmasso Tue, 07 Apr 2015 06:40:13 +0000 - -geonode (2.4.0+beta22) trusty; urgency=high - - [ capooti ] - * [111ace] In layer details page displaying the attributes label and description in place of WPS indicators when OGC WPS_ENABLED is set to False. By default, WPS_ENABLED is now set to False. Refs #1412 - * [09c2ad] We always need to display label and description for layer attribute - - [ Simone Dalmasso ] - * [ade9da] fix the wrong autocomplete box positioning in metadata pages - - [ Jeffrey Johnson ] - * [2df84d] Initial work on describing GeoGig OSM functionality - * [d1f173] Added a new reference page on supported browsers and testing in IE - - [ Simone Dalmasso ] - * [16c3a7] bump flake8 versions (2.2.5 seems to be available anymore) and fix pep8 violations - - [ capooti ] - * [fba8d9] Removed the pyshp dependency, now using OGR - - [ Simone Dalmasso ] - - -- Simone Dalmasso Mon, 09 Feb 2015 15:34:03 +0000 - -geonode (2.4.0+beta21) trusty; urgency=high - - [ state-hiu ] - * [c515cc] Initial support for region filter - * [36fcb3] Collapsed on page load - * [3eebf4] removed binding - * [465c13] fixed count - - [ menegon ] - * [606dd6] Added menegon to AUTHORS - - [ Simone Dalmasso ] - * [8d0dd4] add pyshp to deb dependencies - - -- Simone Dalmasso Fri, 06 Feb 2015 09:13:18 +0000 - -geonode (2.4.0+beta20) trusty; urgency=high - - [ Julien Collaer ] - * [503c96] adding an edit data link pointing to new map - * [4354fa] adding check to show edit data only when stortype is datastore" - * [84c74f] adding a check for the change_layer_data permission" - - [ nathanhilbert ] - * [ec60bf] removed some required win_install_dep packages since wheels are now used - - [ Julien Collaer ] - * [bb05ec] change_layer_data should be given to someone without change_resourcebase granted. - - [ Paolo Pasquali ] - * [575922] Add some style to explore people page - - [ capooti ] - * [bcf0b7] Fixes #1307 by avoiding invalid column names when uploading shapefiles - - [ Julien Collaer ] - * [3d324e] changing links in homepage for registred and unregistred users - - [ Simone Dalmasso ] - * [359692] add badges filter to respect the title search, fixes #1122 - * [a80106] add conditional colon in search result - - [ Nicolas Dufrane ] - * [d5328f] styler does not show up when not logged - - [ capooti ] - * [11d54d] Now regex include "_" - - [ nathanhilbert ] - * [5de107] added ms install instructions and scripts - - [ Nathan Hilbert ] - * [8cd200] Update to readme for windows install build instructions - - [ capooti ] - * [ffa5b9] Relaxing the regex for column names to include some more extra character such as #, : - - [ Paolo Pasquali ] - * [e1ee2e] Truncate long title strings with ellipsis (CSS) - - [ Simone Dalmasso ] - * [a12ebd] workaround for iso xml geoserver links - - [ julien collaer ] - * [a32e21] latest sycing with transifex.com - - [ Matteo Nastasi ] - * [f37500] refactored 'geonode.updateip' script to accomplish #1199 github issue - - [ Geode Team ] - * [62252a] add required value - - [ Simone Dalmasso ] - * [9a78cd] remove confusing id in edit doc metadata - - -- Simone Dalmasso Thu, 05 Feb 2015 16:58:12 +0000 - -geonode (2.4.0+beta19) trusty; urgency=high - - [ Nicolas Dufrane ] - * [0e3955] Fix PEP8 Violations in Pavement.py #2002 - - [ Paolo Pasquali ] - * [a8e6df] Improve explore page for layers, maps and docs - - [ eomwandho ] - * [c58ee4] Editing developer mode installation manual - - [ Paolo Pasquali ] - * [5a760e] Fix IE9 45 rotation - - [ eomwandho ] - * [b285d2] Added postgresql installation in devmode installtion - - [ ilpise ] - * [757e8a] no create gropu button for no auth users - - [ eomwandho ] - * [33826b] Added some titles to list - - [ Jeffrey Johnson ] - * [c02e26] Remove print statements - - [ ilpise ] - * [7426fe] fixed spaces - - [ Simone Dalmasso ] - * [726264] add multi-select to grunt ad assets css - * [4b0bd4] remove the unneeded css rule for style manage page - * [f5892a] fix the legend update on layer detail page - - [ Daniele Viganò ] - * [7f7a2d] Add SITEURL and CATALOGUE to local_settings.py.sample - - [ Ariel Núñez ] - * [7dd349] Added incompatibility notice for IE users (before IE10) - - [ Vivien Deparday ] - * [11a3e0] Update geoexplorer.txt - - [ Simone Dalmasso ] - - -- Simone Dalmasso Wed, 04 Feb 2015 16:30:08 +0000 - -geonode (2.4.0+beta18) trusty; urgency=high - - [ julien collaer ] - * [1275b8] refreshed from tx and compiled - - [ capooti ] - * [a1481a] Added remote services sources in GXP - - [ Paolo Pasquali ] - * [f22642] Add style to edit layer modal - * [568c3c] Close the edit layer modal when opening the styler - * [c6ca90] Add style to edit document modal - * [5c5ce2] Add style to edit map modal - - [ julien collaer ] - * [43bf43] fr translations integrated' - - [ ilpise ] - * [d8c3a3] moving the logo from file field to image field - - [ Paolo Pasquali ] - * [028dc7] Remove html if group logo does not exist - - [ menegon ] - * [4eeb1f] remove files and session if an upload error occurs - * [5b059f] added integration test in order to check if -uploaded- directory is clean - - [ julien collaer ] - * [98ccb9] link added to accounts - - [ Julien Collaer ] - * [c61274] Update contribute_to_translation.txt - - [ nathanhilbert ] - * [07dfb5] Windows quick install update - * [5214a9] added some additional checks and messages for java_paths - * [23e4fb] adding logic to settings.py to check for GEOS and GDAL - * [01f604] updating based on PR #1996 - - [ state-hiu ] - * [9c8b45] CKAN Intents. Added support for: date, caveats. - * [9c1c21] fix typo - * [c5e313] added jetty runner option to setup_geoserver - * [2975cd] Fix for document metadata links - - [ Jeffrey Johnson ] - * [b6552f] Remove/rewrite some incredible stupidity from @jj0hns0n - - [ capooti ] - * [3a6aad] Correctly set LOG_FILE in settings and handle the case where a LOG_FILE is not found - - [ Ariel Núñez ] - * [495d2c] Added creation of geonode_data database. - * [86b6ad] Set 'geonode_data' as the 'datastore' for GeoServer - - [ menegon ] - * [e4cf01] reverted changes - - [ Ariel Núñez ] - * [cc8e35] Improved GeoServer performance - - [ Jeffrey Johnson ] - * [a7c24c] Use the title in the Layer Style Manage page vs the name - - [ Simone Dalmasso ] - * [b93fa0] rework the geoserver_pre_save signal to avoid upload every time, also fixes #1926 - - -- Simone Dalmasso Wed, 04 Feb 2015 11:40:40 +0000 - -geonode (2.4.0+beta17) trusty; urgency=high - - [ Tyler Garner ] - * [a21884] Minor profile page enhancements. Fixes #1975. - - [ Jeffrey Johnson ] - * [e34875] Add an inner try/except block when trying to clean up and delete_from_postgis. Closes #1058 - - [ Tyler Garner ] - * [c2e6b6] Bump `geonode-user-accounts` version to 1.0.10. Fixes #1856. - - [ Simone Dalmasso ] - * [d705ff] add missing comma - - -- Simone Dalmasso Tue, 03 Feb 2015 16:33:57 +0000 - -geonode (2.4.0+beta16) trusty; urgency=high - - [ Tyler Garner ] - * [433cf0] Propagate exceptions from tasks when CELERY_ALWAYS_EAGER is True. - * [75d256] Catch exception when deleting a layer from a layer group. Fixes #1671. - - [ Simone Dalmasso ] - * [9dcab3] fix #1341 - - [ Tyler Garner ] - * [556698] Rename 'Profile Menu' to 'Menu'. Fixes #1898. - - [ Simone Dalmasso ] - * [7cbf3f] fadd back the print service to map detail, fixes #1779 - * [ce7429] update keywords before layer form save, fixes #1196 - - [ Ariel Núñez ] - * [20a3a2] Added LOG_FILE to local_settings.py in packages - - [ Simone Dalmasso ] - - -- Simone Dalmasso Tue, 03 Feb 2015 15:26:42 +0000 - -geonode (2.4.0+beta15) trusty; urgency=high - - [ Paolo Pasquali ] - * [c92f17] Revert layers upload buttons to correct style - * [9eaeb7] Fix group logo image responsiveness in group list page - - [ Jeffrey Johnson ] - * [27f372] Add config["queryable"] = True to the layer detail view so the Identify tool works. closes #1865 - - [ capooti ] - * [c4139d] Bumped geonode-user-messages 0.1.2 - - [ Jeffrey Johnson ] - * [f16853] Actually get a user with importlayers. Closes #1953 - - [ Paolo Pasquali ] - * [f7d05f] Some improvements in people layout and style - - [ Simone Dalmasso ] - * [2b946b] add option to specify remote services name, partially helps for #1961 - - [ Daniele Viganò ] - * [2ba7b5] Users should not be able to edit other users profile - - [ Jeffrey Johnson ] - * [b7373f] Moving test_configure_time to geonode.upload.tests.integration and re-enabling. The tests in geonode.upload.tests.integration are not currently run. @ischneider has proposed to replace this entire module Closes #1767 - - [ Simone Dalmasso ] - * [990c88] make groups selection by title in permissions widget, fixes #1961 - - [ Ariel Núñez ] - * [27e341] Added a context field to UploadSession - * [85e5c3] Better information on the traceback - * [0723a2] Added a LOG_FILE key to OGC_SERVER - * [90378a] Removed help fields on UploadSession - - [ Jeffrey Johnson ] - * [1f4650] Fix up some imports in the upload integration tests - - [ Ariel Núñez ] - * [80a593] Better error reporting - - [ Simone Dalmasso ] - * [60b4f7] fix pep8 - - -- Simone Dalmasso Tue, 03 Feb 2015 11:50:19 +0000 - -geonode (2.4.0+beta14) trusty; urgency=high - - [ Allan Oware ] - * [d4d668] modified about page - - [ d3netxer ] - * [ee28e4] fixed #1803 - - [ state-hiu ] - * [871e53] quick pep8 fix - * [12efdf] support for local geoserver binary - - [ machakux ] - * [f21944] Fix image references in translation contribution docs - - [ Simone Dalmasso ] - - -- Simone Dalmasso Tue, 03 Feb 2015 09:00:45 +0000 - -geonode (2.4.0+beta13) trusty; urgency=high - - [ Paolo Pasquali ] - * [310a6c] Fix #1960 - - [ Simone Dalmasso ] - * [7737d4] disable html5 mode in explorer in search - * [284206] disable notification by default - - [ Jeffrey Johnson ] - * [12c995] Dont use ^ in autocomplete (this means starts with) - - [ Paolo Pasquali ] - * [666036] Add links to homepage facets. And some padding improvements. Fix #1901 - * [ca486f] Remove text-center class from homepage - - [ capooti ] - * [7a71de] Hack to fix #1932 - - [ Paolo Pasquali ] - * [881c43] Add some profile layout fixes - - [ capooti ] - * [9fccf0] Removing stale links when updating layers. Fixes #1090 - * [6818a9] Making flake8 happy - - [ Simone Dalmasso ] - * [97493c] fixes #1888 - - [ Tyler Garner ] - * [e1c1ab] Include step to install `sphinx_rtd_theme` when building docs. - - [ capooti ] - * [1b769b] Fixes #1966 - - [ Simone Dalmasso ] - * [84e8cd] obscure thumbnail urls - - [ capooti ] - * [944446] Reading THEME_ACCOUNT_CONTACT_EMAIL in context processor - - [ Paolo Pasquali ] - * [c2ec51] General minor layout improvements - - [ Jeffrey Johnson ] - * [f0828e] Check if the store.type is None before checking its value - - [ Paolo Pasquali ] - * [6a93e3] Fix form width - - [ Simone Dalmasso ] - * [2dc741] add back the print middleware for private layers fixes #765 - - [ Jeffrey Johnson ] - * [c1c5cc] Delete geoservers store (deleting the postgis table) when doing layer replace (fixes #1860) - - [ capooti ] - * [67f28d] Bumped geonode-avatar 2.1.4 - - [ Simone Dalmasso ] - * [5f1f81] add default configurable permissions for anonymous user - - [ Jeffrey Johnson ] - * [6189f7] Add new logic to detect changing layer types (raster/vector) when replacing layers vs letting geonode handle this - * [06aac1] Use 400 error code vs 500 - - [ Simone Dalmasso ] - - -- Simone Dalmasso Mon, 02 Feb 2015 17:21:05 +0000 - -geonode (2.4.0+beta12) trusty; urgency=high - - [ state-hiu ] - * [ef373f] Initial support for Share with CKAN (HDX as prototype) - * [936b0b] Disable CKAN by default - * [51342f] PEP8 fixes - - [ Tyler Garner ] - * [129000] Enable XFrameOptionsMiddleware for clickjacking prevention. - - [ state-hiu ] - * [a069c7] Do not include admins in view counts - * [0e39c0] layout changes for group members and avatar change - - [ Stefano Menegon ] - * [c20130] Fixed issue with non ASCII characters - - [ state-hiu ] - * [6fc90a] Fix #1847; anonymoususer fix; layout fixes - - [ Daniele Viganò ] - * [b2246b] Fix the README contained in the zip file created by a map download - - [ Simone Dalmasso ] - * [e002a3] use link to docs in readme for installation devmode - * [67b61b] fix link - - [ menegon ] - * [27057b] improved layer_acls performaces - - [ John Jediny ] - * [c85974] Adding blank extra_tab block - - [ menegon ] - * [edb815] PEP8 fixes - * [2fb7d8] used set operations - as suggested by @ischneider - * [f3385a] fixed test_layer_acls issue - - [ Geode Team ] - * [4634b5] research user-group insensitive and exclude AnonymousUser - * [9fbd6b] fix pep8 - - [ state-hiu ] - * [76e43b] Fix dropdown; Prevent duplicate group members - - [ Simone Dalmasso ] - * [a3472c] compile css - - [ state-hiu ] - * [30b305] added documentation to settings.txt - * [2114cb] fixes fakepath issue on document upload - * [785a73] category links to search; more info button - - [ Simone Dalmasso ] - * [94ebf1] simplify notifications - - -- Simone Dalmasso Mon, 02 Feb 2015 10:22:57 +0000 - -geonode (2.4.0+beta11) trusty; urgency=high - - [ state-hiu ] - * [153fdd] Social Origins List; SOCIAL_BUTTONS fallbacks to False - * [f941e2] PEP8 Fixes; Documented settings.SOCIAL_ORIGINS - * [a60b7a] Refractor. Added function to utils.py - - [ Jeffrey Johnson ] - * [cf24b5] Added John Jediny (nepanode) to contributors. - * [02f724] Adding slack config to travis.yml - - [ George Silva ] - * [340d6e] Fix bug that causes paver_start to fail on windows. - - [ Geode Team ] - * [bf2353] add perm choice for #1922 - * [4e5c81] replace is_layer_list by is_layer - - [ capooti ] - * [9e0b8d] This fixes #1908 - * [9519d3] Oops, forgot about the display_order in previous commit - * [e74388] Default value for display order cannot be null - * [5a3d35] Added an integration test for set_attributes, used by updatelayers - - [ Tyler Garner ] - * [65e191] Minor improvements to the `groups` tests. - - [ state-hiu ] - * [e1ce41] Fix #1849 - * [986a9d] Fix #1934 - * [63726b] PEP8 Fix - * [1300c6] Activity Filters (All, Layers, Maps, Comments) - * [bcc714] PEP8 Fixes - - [ Simone Dalmasso ] - * [e79ce7] add celery to debian dependencies - - -- Simone Dalmasso Fri, 23 Jan 2015 13:55:26 +0000 - -geonode (2.4.0+beta10) trusty; urgency=high - - [ Mila Frerichs ] - * [996276] Add foreground option to startup script to start django in foreground - * [889056] Add Dockerfile and fig for easier developement/deployment - - [ Ariel Nunez ] - - -- Ariel Nunez Thu, 15 Jan 2015 10:10:36 -0500 - -geonode (2.4.0+beta9) trusty; urgency=high - - [ Tom Kralidis ] - * [143a06] update pycsw docs ref [ci skip] - - [ Tyler Garner ] - * [8bb675] Bump Django to 1.6.10. - - [ Ariel Nunez ] - - -- Ariel Nunez Thu, 15 Jan 2015 10:09:39 -0500 - -geonode (2.4.0+beta8) trusty; urgency=high - - [ Simone Dalmasso ] - * [c8f5f5] add initial thumbnail logic - * [b63a85] add thumbnail to map save - * [79d6d4] cleanup map thumbnails files on regen - * [63c5c4] formalize thumb name - - [ Ariel Nunez ] - * [fe1e18] Fixed short version of account requested - * [c4cb6d] Renamed capital PNG to png in docs - * [fe5b5f] Renamed capital PNG to png in docs - - [ Tom Kralidis ] - * [e62edb] add CSW bbox query test - - [ Simone Dalmasso ] - * [b64467] simplify thumbnail management - * [1fef11] save layer only once - * [af42b6] fix tests and folder creation - * [227934] Save the remote thumbnail url on layer load - * [ae6eea] move the create logic in helpers - * [007573] make the http_client not tied to gs - - [ Paolo Pasquali ] - * [608773] Change the profile dropdown in bootstrap modal - - [ Tyler Garner ] - * [c13513] Return the user's full name in the resolve_user and layer_acls views. - - [ Paolo Pasquali ] - * [52bf1d] Add links to profile page - - [ Tyler Garner ] - * [f7f9c7] Update skip_unadvertised logic to not run when advertised is "false". - - [ Ariel Núñez ] - * [12b3fb] Update ec2.py - - [ Tyler Garner ] - * [5a89f6] Update the upload progress bar to use bootstrap 3 class names. Fixes #1797. - * [595a8f] Bump django-forms-bootstrap to 3.0.1 for bootstrap 3 classes. - * [112222] Minor style improvements and code cleanup. - * [b1e95a] Fix autopep8 shenanigans in the url routes. - * [f1f783] Improve upload button styles. - * [e27326] Improve style of the attribute table on the layer metadata route. - - [ George Silva ] - * [e00168] corrigindo algumas traduções. - - [ Tyler Garner ] - * [9b82d7] Use client-side polling (instead of server-side) polling when using the importer upload. - - [ menegon ] - * [8ab593] fixed the pluralization of translated strings - - [ Tyler Garner ] - * [c38352] Fix a bug where clicking 'Clear' on the upload page would fail to clear the actual files. - * [b44281] Use a single paver task to execute all of the tests. - * [91a9c1] Fix a broken url on the upload layer page. - * [b4e47a] Rename GeoGit to GeoGig. - * [1000ca] Initial celery integration. - * [fb2973] Use an SLD's name instead of text from a name element when saving styles in GeoNode. - - [ Simone Dalmasso ] - - -- Simone Dalmasso Wed, 14 Jan 2015 07:30:09 +0000 - -geonode (2.4.0+beta7) trusty; urgency=high - - [ Ariel Nunez ] - * [1790e4] Updated setup.py based on ppa versions - - [ Andres Lemon ] - * [f9af9d] Corrected some of .txt documents - * [bd017f] Removed images with PNG in capital letters - - [ Ariel Nunez ] - * [83dcee] Added dependency on python-geolinks needed by latest pycsw - - -- Ariel Nunez Sun, 07 Dec 2014 16:31:30 -0500 - -geonode (2.4.0+beta6) trusty; urgency=high - - [ capooti ] - * [a965cf] For some reasons this file was not merged in PR #1811 - - [ Julien Collaer ] - * [673ead] Update views.py - - [ capooti ] - * [986468] Added the test required by @ingenieroariel in #134 - - [ DHohls ] - * [beecfe] Bumped version dependency for OWSLib - - [ Andres Lemon ] - * [266053] Minimal modifications to the comm_bylaws file - - [ capooti ] - * [d511e1] Fixes #1858, also for maps and documents - - [ Simone Dalmasso ] - * [531afc] move ng-cloak css out from extra-head, fixes #1871 - * [ec22fa] add border and margin to manage styles - * [d25101] make style manage boxes to be adjacent, fixes #1863 - * [0cacda] unify _action in all templates, fixes #1781 - * [ef86e5] remove some lad and unused templates - * [274842] remove django_templatetags from dependencies - * [39f43b] remove unneeded tags (fix tests) - - [ capooti ] - * [7eec87] Bump geonode-user-accounts 1.0.8, fixing #1857 - - [ Andres Lemon ] - * [776af0] Modified some images in tutorial - - [ Simone Dalmasso ] - * [3a1a88] hide the_eom from attributes, fixes #1859 - - [ Andres Lemon ] - * [889e03] Fixed .png - - [ Ariel Nunez ] - * [b5429f] Add missing messages block to base.html - * [418177] Support activating accounts by admin users. - - [ Andres Lemon ] - * [57a9a0] More screenshots added in tutorial - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 05 Dec 2014 15:19:06 -0500 - -geonode (2.4.0+beta5) trusty; urgency=high - - [ Ariel Núñez ] - * [c7941b] Update ec2.py - - [ state-hiu ] - * [6a2781] added update button to top. Updated page headers for documents and maps - - [ Paolo Pasquali ] - * [766414] Work on Responsive header - - [ Simone Dalmasso ] - * [2cc2f2] fix the document upload link to resource - - [ Paolo Pasquali ] - * [c38c25] Fix header dropdown sign in form style - - [ Matt Bertrand ] - * [5dc0b5] "Manage Layers" and "Replace this Layer" should not be available for remote service layers - - [ Simone Dalmasso ] - * [23f21b] make search by title case insensitive - * [492c3a] hide some fields from the layer metadata, fixes #1850 - - [ Geode Team ] - * [29626b] adding rule for controls>div - - [ Andres Lemon ] - * [8c1e7b] Removed ubuntu version in pdf instructions - - [ Simone Dalmasso ] - * [1aaf3b] add local geoserver to map thumbnail create - - -- Simone Dalmasso Thu, 27 Nov 2014 14:33:12 +0000 - -geonode (2.4.0+beta4) trusty; urgency=high - - [ capooti ] - * [614930] Restored the notification app and adding some more notification types - * [f5b19e] Added support for i18n - - [ state-hiu ] - * [82fdb4] fixes #1830 - * [52c3ab] whitespace fixes - * [51e468] Fixes #1835 - - [ Simone Dalmasso ] - * [6a0a27] show categories if they have records - - -- Simone Dalmasso Tue, 25 Nov 2014 14:22:18 +0000 - -geonode (2.4.0+beta3) trusty; urgency=high - - [ Matt Bertrand ] - * [b8a96c] Always convert to web mercator coordinates - - [ state-hiu ] - * [d9101e] fixes #1783 - * [ab667c] fixes #1723 - * [d516ef] fixes #1785 - * [7158b9] fixes #1807 - - [ Ariel Nunez ] - * [171649] Updated locale files - - [ Matt Bertrand ] - * [15e948] pep8 - - [ Simone Dalmasso ] - * [0152f1] order keywords alphabetically and fix typo - - [ Paolo Pasquali ] - * [0c9feb] General improvements in page layouts - * [76659d] Improving page layouts - - [ state-hiu ] - * [fe49f5] fixes #1768 and #1777 - * [e44ec3] Fixes #1780 - - [ Paolo Pasquali ] - * [3f8a75] General Style fixes on page templates - - [ Tom Kralidis ] - * [27519e] bump pycsw/OWSLib dependencies - * [b6a47f] fix CSW test - * [b43a9d] fix indent - - [ Simone Dalmasso ] - * [2a6a9c] few fixes to the search results - - [ Paolo Pasquali ] - * [9b0855] Style fixes - * [13eb90] Fix .fa class margin in filters - - [ Simone Dalmasso ] - - -- Simone Dalmasso Mon, 24 Nov 2014 14:40:21 +0000 - -geonode (2.4.0+beta2) trusty; urgency=high - - * [ea214b] Added Sinhala and Tamil - * [8f10e3] Added languages that are not well supported in Django - - -- Ariel Nunez Wed, 19 Nov 2014 17:55:40 -0500 - -geonode (2.4.0+beta1) trusty; urgency=high - - * [ec9bf4] The thumbnail url can be longer than 255 chars - - -- Ariel Nunez Wed, 19 Nov 2014 13:13:21 -0500 - -geonode (2.4.0+alpha38) trusty; urgency=high - - [ Paolo Pasquali ] - * [d6a8bb] Fix Layer Style Manage Page Layout and HTML Select Style - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 19 Nov 2014 12:36:37 -0500 - -geonode (2.4.0+alpha37) trusty; urgency=high - - * [2c8ea7] Fixed E126 continuation line over-indented for hanging indent - * [6f7d2d] Added link to PUBLIC_LOCATION in map save - - -- Ariel Nunez Wed, 19 Nov 2014 12:29:21 -0500 - -geonode (2.4.0+alpha36) trusty; urgency=high - - * [febb21] Fixed map thumbnail saving - * [0ccd95] flake8 - - -- Ariel Nunez Wed, 19 Nov 2014 11:51:35 -0500 - -geonode (2.4.0+alpha35) trusty; urgency=high - - [ state-hiu ] - * [39f5a4] Levels, V2 - - [ Matt Bertrand ] - * [e1157c] Fix lazy loading in geoexplorer detail pages, fix service layer issues - - [ Simone Dalmasso ] - * [d7616d] bump announcement version and update css - - [ Geode Team ] - * [224597] unstranslating name var for Remote Thumbnaill - * [0d3276] Outsourcing css GeoExplorer - - [ Ariel Nunez ] - * [47d576] Improved logic on gxp configuration - - -- Ariel Nunez Wed, 19 Nov 2014 10:31:58 -0500 - -geonode (2.4.0+alpha34) trusty; urgency=high - - [ Ariel Nunez ] - * [9d30a6] Fixed pyflake issues. - - [ Jeffrey Johnson ] - * [a6fcee] Fix map.zoomToExtent - - [ Ariel Nunez ] - - -- Ariel Nunez Tue, 18 Nov 2014 15:15:31 -0500 - -geonode (2.4.0+alpha33) trusty; urgency=high - - [ Paolo Pasquali ] - * [5124a6] Fix paneltbar margin in maps page - * [29657f] Fix geoexplorer body font - - [ Ariel Nunez ] - * [317651] Disabled lazy loading, refs #1795 - * [197477] Zoom to extent instead of bbox (temporary), refs #1795 - - -- Ariel Nunez Tue, 18 Nov 2014 14:14:12 -0500 - -geonode (2.4.0+alpha32) trusty; urgency=high - - [ Geode Team ] - * [c53480] corection 1771 - * [893255] put last modification on less file - - [ capooti ] - * [c44d5a] Added a request download button in layer and document detail pages. The request will be sent to resource owner using django-notification - * [6f5abc] Making flake8 happy. Using get_object_or_404 for getting the resource - * [0a419a] Refactored the RESOURCE_PUBLISHING setting, now it is not activated by default - - [ state-hiu ] - * [26bee7] initial levels support - - [ Humanitarian Information Unit ] - * [e21c49] Fix minor typo in comment - - [ Simone Dalmasso ] - * [c45b9b] update integrations tests for resource publishing - * [bca79e] update base.css - * [bec1d9] bump announcements version - - [ Ariel Nunez ] - - -- Ariel Nunez Tue, 18 Nov 2014 09:20:21 -0500 - -geonode (2.4.0+alpha31) trusty; urgency=high - - [ Simone Dalmasso ] - * [8303b2] fix search by date using datetimepicker - - [ Ariel Nunez ] - * [ef614b] Set LAYER_PREVIEW_LIBRARY = 'geoext' as default, refs #1528 - - -- Ariel Nunez Mon, 17 Nov 2014 09:19:21 -0500 - -geonode (2.4.0+alpha30) trusty; urgency=high - - * [7a4155] Updated translations - - -- Ariel Nunez Fri, 14 Nov 2014 13:41:02 -0500 - -geonode (2.4.0+alpha29) trusty; urgency=high - - * [7c4cfd] flake8 on services/views.py - * [fb4b2e] Updated locale files - * [69012d] Updated locale - - -- Ariel Nunez Fri, 14 Nov 2014 10:35:54 -0500 - -geonode (2.4.0+alpha28) trusty; urgency=high - - [ capooti ] - * [b37a1b] First raw implementation of published/unpublished resource - * [7dc3e3] Refuse from previous commit - * [0128bf] Returning 404 for an unpublished resource - * [c42312] Testing the search API with unpublished layers - * [c0e177] Testing that layer_detail returns 404 for an unpublished layer - * [d0a8c3] Added some basic documentation on publishing/unpublishing resources - * [050c6b] Forgot a documentation image from previous commit - * [a774d4] Making flake8 happy - * [b58499] Added a RESOURCE_PUBLISHING to enable/disable resource unpublishing for django staff members - * [715d8b] Now when unpublishing the layer is unadvertised in GeoSever. Users with publish_resourcebase can still access the layer detail page. - * [842242] Moved integration permissions test to its appropriate TestCase - - [ Simone Dalmasso ] - * [8a1675] add ngcloak to resource base snippet, fixes #1768 - - [ capooti ] - * [7520e3] Fixed a couple of strings in permission forms and updated IT messages related to permissions - - [ Ariel Nunez ] - * [1bf35f] Take countries out from translations - * [1f9817] Updated translation strings - - -- Ariel Nunez Fri, 14 Nov 2014 10:04:22 -0500 - -geonode (2.4.0+alpha27) trusty; urgency=high - - * [bce0ed] Make flake8 happy - - -- Ariel Nunez Thu, 13 Nov 2014 15:27:33 -0500 - -geonode (2.4.0+alpha26) trusty; urgency=high - - * [086481] Disable test_configure_time temporarily, refs #1767 - * [2fcb1d] Make flake8 happy - * [b6cc3d] Pass geoserver variable to map download to avoid incorrect redirection - * [4f6121] Pass geoserver variable to map download to avoid incorrect redirection - - -- Ariel Nunez Thu, 13 Nov 2014 13:13:21 -0500 - -geonode (2.4.0+alpha25) trusty; urgency=high - - * [6fa68e] Adedd python-django-bootstrap3-datetimepicker to the control file - - -- Ariel Nunez Thu, 13 Nov 2014 11:37:21 -0500 - -geonode (2.4.0+alpha24) trusty; urgency=high - - [ Ian Schneider ] - * [99e8db] upload cleanup + tests - * [b12ad5] uploader integration tests running and passing - * [2c1272] continued upload fixes - * [dfb20f] uploader client-side fixes - - [ Ariel Nunez ] - * [263622] Upgrade to gsconfig 0.6.11 - * [d52294] Updated integration tests to try to fix timezone issue - * [60c077] Updated integration tests to try to fix timezone issue (second time) - * [8a75a7] Fixes map download - - -- Ariel Nunez Thu, 13 Nov 2014 11:33:15 -0500 - -geonode (2.4.0+alpha23) trusty; urgency=high - - [ Simone Dalmasso ] - * [9b32f6] comment the amharic language - * [4cf5ad] limit the model translation language to english by default. Add the settings for translated languages. - * [c5f49c] add missing...space - * [92de8f] clear map thumbs logic - - [ Ricardo Garcia Silva ] - * [c4c42d] changed temporal extent fields to DateTime - * [6a04a4] fixed temporal extent formatting in the xml template - * [a7c49a] fixed some pep8 errors reported by flake8 - * [f350f6] added datetime support for temporal extent fields in layers, maps and documents - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 12 Nov 2014 14:48:35 -0500 - -geonode (2.4.0+alpha22) trusty; urgency=high - - [ Simone Dalmasso ] - * [1bc5a7] save the system assigned name for uploaded file and use it for the rest - - [ Ariel Nunez ] - * [e4e6bc] Added requested languages from transifex - * [1e7d1f] Updated translations - * [4930f6] Added messages for new languages - * [27e8dc] Updated .mo - * [888705] Updated .po from transifex - * [42f63f] Updated locale with dummy data so they are picked up by tx push - * [65bb12] Used google's workbench to translate 30% of arabic - * [262e39] Manual fixes to arabic django.po - * [b8a60f] Updated arabic - - -- Ariel Nunez Tue, 11 Nov 2014 18:45:53 -0500 - -geonode (2.4.0+alpha21) trusty; urgency=high - - [ CORTI Paolo ] - * [fea76b] Added geonode custom permissions definition for ResourceBase and Layer - - [ capooti ] - * [08ba0c] Added geonode custom permissions definition for ResourceBase and Layer - - [ CORTI Paolo ] - * [f5b43e] Now we manage custom perissions from the user interface - - [ capooti ] - * [3cf149] Now we manage custom perissions from the user interface - * [996e81] An user without the download_resourcebase permission cannot download a layer, map or document - * [5347fe] An user without the download_resourcebase permission cannot download a layer, map or document - * [051f1f] Display correct options in edit window (edit metadata, styles...) given the permissions the user has for the layer - * [3c6088] Display correct options in edit window (edit metadata, styles...) given the permissions the user has for the layer - * [6eda03] Returning a 401 if user want to modify metadata but has not a change_resourcebase_metadata permission - * [54e3ee] Returning a 401 if user want to modify metadata but has not a change_resourcebase_metadata permission - * [fcaf51] Returning a 401 if user want to view a layer but has not a view_resourcebase permission. Using gettext_lazy messages - * [f6058f] Returning a 401 if user want to view a layer but has not a view_resourcebase permission. Using gettext_lazy messages - * [2e1bf7] Enable edit in gxp if user has change_layer_data permission on the layer - * [415894] Enable edit in gxp if user has change_layer_data permission on the layer - * [ed3090] Now the document details page is update with the new custom permissions structure - * [c565ff] Now the map details page is updated with the new custom permissions structure - * [06ab3b] Returning a 404 if user want to view map but has not a view_resourcebase permission - * [7ff857] Returning a 401 if user want to view a layer but has not a view_resourcebase permission - * [d18385] Permissions form must display edit layer data and styles fields only if resource is a layer - * [cd1a6c] Fixing a bug: we were incorrectly removing specific layer permissions from resource base instead than from layer - * [3e4950] This is to implement, from django, the change_layer_style permission application, by intercepting the requests to GeoServer REST API - * [298453] Lets have any authenticate user able to create a style - * [accff2] Now the change_resourcebase, delete_resourcebase, change_resourcebase_permissions, publish_resourcebase are managed from a single access field in permissions form - * [42f059] Rendering the standard 401 page for an unauthorized acces to the edit metadata page - * [69ff43] Updates default permissions with new custom permissions and test them - * [61769b] Added a is_layer template variable in order to show/hide custom layer permissions in upload layer/document - * [9b5fe0] Managing default layer permissions causing a problem when uploading - * [c84803] Download links must be sent to template only if user has download_resourcebase permission - * [26beb0] Rendering the standard 401 page for an unauthorized acced to the replace layer page - * [1f783c] Make resolve_object correctly working when the permission must be checked on the original object (layer) and not on the resourcebase - * [cd39f5] The layer_style_manager view must return a 401 if the user has not the change_layer_style permission - * [0b45e5] Added a bunch of tests for testing new custom permissions when the user is authenticated but not a superuser - * [dd7adb] Layer replace view must pass is_layer for permission form to works correctly - * [1d8512] Adding tests on permission for anonymous user - * [d0db4c] Removing default layer permissions, as they should be never used - * [d09fcd] We need to remove view_resourcebase from anonymous group when testing it on anonymous user - * [4e9964] Added integration tests for view_resourcebase - * [07551b] Fixed permission checks on views after master sync - * [aba65c] Restore a previous unwanted line removal - * [5966c5] Now integrations test are properly running - * [c1df98] Updated document views with new permission system - * [f0874b] Moved all permissions tests from layers to security - * [0cc0b3] Updated acls and its relative test with new change_layer_data permission - * [8b40c4] Updated documentation relative to permissions (more to be done!) - * [6c73bc] Updated maps views and template to new custom permissions - * [8a9e0f] Syncing with master - * [c1008b] Syncing with master. Forgot locale files - * [fed6b2] Syncing with master. Forgot locale files (2) - * [1c3619] Reverting to master version for this file - * [8452b2] Removed an unused try/permissiondenied block, and uniformed the request variable name to the same way as in other views - * [b888ab] Removed an unused try/permissiondenied block - * [509e9d] Removed unused permissions from layer permission check - * [886003] Had to set this again, otherwise tests are failing. Will investigate later - * [c53296] Making flake8 happy - * [2d29b7] Making flake8 happy on a last file - - [ Ariel Núñez ] - * [72ae02] Disabled paver static in Travis - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 05 Nov 2014 10:30:00 -0500 - -geonode (2.4.0+alpha20) trusty; urgency=high - - [ x ] - * UNRELEASED - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 05 Nov 2014 08:58:25 -0500 - -geonode (2.4.0+alpha19) trusty; urgency=high - - [ x ] - * UNRELEASED - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 05 Nov 2014 08:55:39 -0500 - -geonode (2.4.0+alpha18) trusty; urgency=high - - [ Tom Kralidis ] - * [4a29cb] s/TC211/ISO/ as per https://groups.google.com/forum/#!topic/geonode-users/e7LnCtPBdKs - * [3c27c1] fix flake8 error - - [ Raphael Sprumont ] - * [eb3f4d] fix order translate js - - [ Simone Dalmasso ] - * [f42ca8] correctly use the filename ad fallback in document title - * [c49ca3] vix the err403 handler for non allowed users and the admin_contact method for resourcebase - * [684606] fix integration test to respect the new error code - - [ Jeffrey Johnson ] - * [d0ccfe] adding screenshot of demo site to organizational/about.txt - - [ Stefano Menegon ] - * [e24ce6] Fixed some layout issues on GeoExplorer - - [ Geode Team ] - * [0b7726] corection_1718 - - [ Ariel Nunez ] - * [6e9b8c] Avoid inifite .git symlink when applying workaround for git-dch bug - - -- Ariel Nunez Wed, 05 Nov 2014 08:28:09 -0500 - -geonode (2.4.0+alpha17) trusty; urgency=high - - [ Ian Schneider ] - * [29c726] safer,complete sync of emailaddress/profile, tests - - [ giohappy ] - * [eef970] enable Edit styles menu item only if geoext is set as preview library - - [ Simone Dalmasso ] - * [147bd5] remove the files on layer delete. - - [ Julien Collaer ] - * [4a3fcc] tags - - [ Simone Dalmasso ] - * [1dd85f] remove the ng directive in profile detail fixes #1744 - * [c8d60b] fix flake8 - * [5c1858] fix flake8 - * [c4f571] avoid on load date query trigger - * [f96fad] bump angular to 1.3.0 and leaflet-directive to 0.7.9 - * [8c13e9] prevent default event on bulk perms form - - [ Raphael Sprumont ] - * [9f4521] correction issue 1743 Geonode - - [ Simone Dalmasso ] - * [8ca828] fix service url reverse - - -- Simone Dalmasso Wed, 29 Oct 2014 14:54:26 +0000 - -geonode (2.4.0+alpha16) trusty; urgency=high - - [ x ] - * UNRELEASED - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 24 Oct 2014 11:40:11 -0500 - -geonode (2.4.0+alpha15) trusty; urgency=high - - * [e4f9cd] Bumped django-user-account - * [570894] geonode-user-accounts is now version 1.0.5 - * [620997] Replace urgency correctly - - -- Ariel Nunez Fri, 24 Oct 2014 11:35:33 -0500 - -geonode (2.4.0+alpha14) trusty; urgency=high - - [ Simone Dalmasso ] - * [7de1b9] remove the navbar-form class from search form - - [ Julien Collaer ] - * [7ba57e] adding translations items - * [8e5c04] using interpolate syntax for incrusting var into translables texts - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 24 Oct 2014 10:15:18 -0500 - -geonode (2.4.0+alpha13) trusty; urgency=high - - * [9e4552] Fixed missing dash in pip install - - -- Ariel Nunez Wed, 22 Oct 2014 23:35:11 -0500 - -geonode (2.4.0+alpha12) trusty; urgency=high - - * [e2d977] Decrease verbosity level in installer - * [02217c] Removed unused popd - - -- Ariel Nunez Wed, 22 Oct 2014 23:08:05 -0500 - -geonode (2.4.0+alpha11) trusty; urgency=high - - * [08f7dd] Fixed recursive .git symlink - * [9451d4] Move to postgis2 on Ubuntu 14.04 - - -- Ariel Nunez Wed, 22 Oct 2014 22:30:07 -0500 - -geonode (2.4.0+alpha10) trusty; urgency=high - - [ x ] - * UNRELEASED - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 22 Oct 2014 22:05:10 -0500 - -geonode (2.4.0+alpha9) trusty; urgency=high - - [ Ian Schneider ] - * [608fe9] fix order of arguments to metadata_links - * [2931e6] some navbar tweaks for small devices - - [ Julien Collaer ] - * [b618b2] adding fr translations - - [ Ariel Núñez ] - * [f20ec7] Update setup.py - - [ cristinao ] - * [cae3b8] Update tour.txt - - [ Ariel Núñez ] - * [454a1c] Added Cristina Ospino to AUTHORS - * [1c9052] Added Patrick and Derek to the PRIMARY AUTHORS - * [7bebbf] Don't enable the default site after removal - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 22 Oct 2014 21:55:45 -0500 - -geonode (2.4.0+alpha8) trusty; urgency=high - - * UNRELEASED - - -- Simone Dalmasso Tue, 21 Oct 2014 14:24:39 +0000 - -geonode (2.4.0+alpha7) trusty; urgency=high - - [ Simone Dalmasso ] - * [e43839] fix css for dropdow - * [c98091] set the map local geoserver url after the local_settings import - - [ Ian Schneider ] - * [9d5b24] rename angular search components for clarity - - [ Simone Dalmasso ] - * [ab5dc8] add bulk permissions for registered users - * [c3b4d7] use the valid layer name as file name on the disk - * [545cdf] fix the group member add, use join. Fixes #1717 - * [8f4dae] don't trigger save twice in update layers, set correctly the bbox. - * [c66526] remove erroneous MAP_BASELAYERS definition in local_settings. - - [ Ian Schneider ] - * [bf2e9a] remove container width breaking responsive layout - - [ Simone Dalmasso ] - * [a4a11b] use bootstrap alerts to notify the bulk perms submit, and better resource select method - * [940a26] fix test - - [ Julien Collaer ] - * [de8176] adding translation tags - - [ Minpa Lee ] - * [2f0b85] added korean language support - - [ Simone Dalmasso ] - * [daba02] remove unwanted ng-if in keywords list - * [319726] wrap profile items in a div - - [ Ariel Núñez ] - * [6b9738] Update settings.py - - [ Ariel Nunez ] - * [a87921] Updated locale files with makemessages and compilemessages - * [03b303] Another translations update - * [daa3eb] Added some target languages for the next release before contacting the translators - * [32edf2] Updated list of languages - - [ Simone Dalmasso ] - * [580c9f] add back the ng-if in the right place in keywords - * [af884e] comment out the tl language for the moment, add myself to the travis mails - * [3b2eb9] fix comment, for pep8 - * [de5ba2] add permissions filter to resources counts in profile list - - -- Simone Dalmasso Tue, 21 Oct 2014 14:19:29 +0000 - -geonode (2.4.0+alpha6) trusty; urgency=high - - [ Matthew Hanson ] - * [526dfe] fix remote services thumbnail creation - * [4b385c] fixed layer id in remote service create thumbnail - - [ Tyler Garner ] - * [6360db] Fix unclosed and orphaned markup elements. - * [ce48dc] Resource contact elements should not overflow the sidebar. - * [648492] Apply more restrictive selector to date picker styling. - * [69727a] Allow profiles to be ordered by the username and join date. Fixes #1608. - - [ Simone Dalmasso ] - * [5233b9] remove model translation version for now and add the fallback language setting - - [ Tyler Garner ] - * [140a97] External service views should use consistent GeoNode style. - * [98bb59] Fix typo in the service model. - * [9c045c] Close an unclosed

    element in the layer_remove template. - * [2b5b68] Fix the travis error. - - [ Matt Bertrand ] - * [598b84] Fix layer lookup (specify service=None for local layers) - - [ capooti ] - * [4a3b21] Added the --skip-geonode-registered option in updatelayers. - - [ Matt Bertrand ] - * [55a723] Don't cascade delete from geoserver for remote layers. Fixes #1619, #1620. - - [ Tyler Garner ] - * [dc3422] Move the Register link inside of a navbar list. Remove unneeded
tag. - - [ Micah Wengren ] - * [676b17] Add a the Leaflet.Fullscreen plugin as a default with Leaflet preview on layer_detail view - - [ menegon ] - * [5aaac2] made some strings translatable - * [cb7265] Other strings marked as translatable - - [ mwengren ] - * [c91276] Use Javascript to detect plugin presence - * [1a4360] Modify Links model to generate GetCapabilities links for WMS, WFS, WCS services - * [548938] Restored 'OWS' link, but with added params for GetCapabilities response - - [ Matthew Hanson ] - * [a9ae42] fixed permission check for publishing wms - - [ Ian Schneider ] - * [3f5a00] make flake8 happy except long lines - - [ state-hiu ] - * [85ad7a] added license_help_text - * [f33a85] document type (subtype) search - * [2ed2af] added doc file types - * [904cfc] added archive facet - - [ capooti ] - * [264e3b] Satisfying a pep8 recommendation. - - [ state-hiu ] - * [bf5106] PEP8 Fixes for E128 - * [e63f28] small PEP8 fix - - [ Brylie Christopher Oxley ] - * [941162] Moved populate_maplayers, adjusted import statement in tests.py. - - [ state-hiu ] - * [21e823] final pep8 fixes - * [976653] fixed a few license layout issues - - [ Ian Schneider ] - * [9e76ca] minor search changes to support reuses - - [ state-hiu ] - * [0a2da5] added download button and linked images - * [d29186] minor change to html5 data attributes - * [4d644c] fixes to document views - * [cf6ae0] PEP8 fix - * [253bad] add group email, hide manage commands, group logo css - * [771714] revert css change b/c of less issues - * [34a770] PEP8 fix - * [a59375] fixed whitespace - * [6b739b] added error messages. fixed title - * [f1bb5f] suggested changes made - * [07e17b] group activitu feed, members layout, help_text, fixes - * [8ab3ee] PEP8 fixes - - [ mwengren ] - * [a51e43] Revert to bare WMS, WFS, WCS URLs in base_link, update gmd:protocol and other element values in ISO template - - [ Tom Kralidis ] - * [10d776] remove query string prefix - - [ state-hiu ] - * [a4b1e2] reworked profile contact info and layout - * [3fadaa] added in groups section to profile page - * [c88c92] PEP8 fixes - * [eb0997] added in user activities link - * [0005c6] added pptx - * [31a19d] PEP8 fix - * [12db36] updated upload layout for documents and layers - * [751ddd] PEP8 fix - - [ Tom Kralidis ] - * [5bf7df] add tests, remove erroneous link add __str__ for Link class - * [f8aadd] remove commented out code - * [b43bfa] remove unused var - * [6ce22f] remove unused var - - [ Ian Schneider ] - * [08d150] provide postgis as dbtype for datastore - * [d920a5] upload - log unexpected exceptions - - [ mwengren ] - * [24dfb7] Don't assume a 'local' MapLayer has been added to GeoNode from GeoServer - * [96497f] Make flake8 happy, remove unused import - - [ Ian Schneider ] - * [237f2c] reformat logging settings - - [ Tom Kralidis ] - * [a15f86] Update models.py - - [ state-hiu ] - * [288825] fixed geoext preview map center - - [ Tyler Garner ] - * [bfa4a6] Remove the AUTH_PROFILE_MODULE setting. - - [ mwengren ] - * [afdbb1] Change Foreign Key relationship for ResourceBase and Thumbnail - - [ Tyler Garner ] - * [16aa73] Update comment functionality to work with Bootstrap 3. - - [ Brylie Christopher Oxley ] - * [1be8a4] Updated documentation steps. Fixes #1194 - - [ Matthew Hanson ] - * [7de084] cleaned up redundant code for resolving map permissions - - [ Ian Schneider ] - * [e0bc74] announcements as template include fragment - - [ Simone Dalmasso ] - * [85607b] don't load assets.min.css in debug_static mode - * [fbbd18] fix css minification order - * [0ce0b5] make the url sync back to work - * [d42fa9] DRY the address logic - * [3125b6] use anonymous group to manage anonymous permissions, fixes permissions for newly created users - * [6b62f5] make flake8 happy - * [6f521d] add test for new anonymous group permissions, fix set_default_permissions and group tests - - [ Jean Jordaan ] - * [eec65f] Note docs on geoserver auth; also some typos - - [ joebocop ] - * [5c08e1] Modify wording of index.txt - - [ Simone Dalmasso ] - * [673215] distance profile resources count. Fixes #1651 - * [8664aa] check if the user has the perms to save an existing map. fixes #1645 - * [8f5b08] keep in sync the emailaddress between profile and django accounts - * [f417d5] don't trigger email address registration with tests, they do it already - * [5ea27c] add back the create map link in layers snippets - * [7928b4] add back popular_count and fix create map icon - * [505bd7] make flake8 happy - * [b85446] exclude AnonymousUser from autocomplete fixes #1653 - - [ npmiller ] - * [c0c8bc] Wrap tabs in a block in the base template - - [ Simone Dalmasso ] - * [3333f8] don't trigger the save on popular_count update, fixes #1631 - * [14e8fc] don't run replace in grunt tasks, it breaks the css - * [3dbf3a] remove no longer needed bootstrap 2 css - * [3c195a] small groups bugs fixing - - [ Stephen Mather ] - * [3921c8] add-apt-repository not installed by default 12.04 - - [ Luiz Vital ] - * [012ba5] Fixes geoserver_rest_proxy view to be able to send non-ascii chars - - [ Taro Matsuzawa ] - * [3a4b48] save map use unicode chars - * [4873af] fixes config file name - - [ Stephen Mather ] - * [62a7fc] Update README - * [eea74d] fix capitalization for Ubuntu - * [9d1182] Clarifying language Ubunti 12.04 / 14.04 install - - [ Matt Bertrand ] - * [4d520b] Don't allow import of ArcGIS MapServer layers that aren't in a projection compatible with web mercator. - - [ state-hiu ] - * [3b567f] fixed location for document popular_count - - [ Micah Wengren ] - * [d8239f] Add leaflet-fullscreen plugin resources to bower and grunt config files - * [868b17] Update static resources with leaflet-fullscreen plugin files - - [ Simone Dalmasso ] - - -- Simone Dalmasso Tue, 07 Oct 2014 14:40:24 +0000 - -geonode (2.4.0+alpha5) trusty; urgency=high - - [ Tyler Garner ] - * [3181bf] GeoGit link logic only runs when the layer's store type is a dataStore. - - [ Simone Dalmasso ] - * [10952b] make sure the signals are not attached when running geonode-updateip - - [ Tyler Garner ] - * [d1af86] Update permission form select2 options to allow the input fields to be overridden in css. - * [f8c6c2] Minor CSS fix for the filter elements. - * [800990] Consistently use the SOCIAL_BUTTONS context to toggle social_link visibility on detail pages. - - [ Simone Dalmasso ] - * [56cafb] use lat_lon instead of native projection in updatelayers - - [ Tyler Garner ] - * [720b4d] Move Bootstrap higher in the compiled CSS. - - [ Simone Dalmasso ] - * [bd11f4] use decimal in update layers and bump minor version to 2.4 - - -- Simone Dalmasso Mon, 28 Jul 2014 07:02:39 +0000 - -geonode (2.4.0+alpha4) trusty; urgency=high - - * [900e91] make flake8 happy and import Datasource in the method (install would break) - - -- Simone Dalmasso Wed, 23 Jul 2014 10:08:42 +0000 - -geonode (2.4.0+alpha3) trusty; urgency=high - - [ Tyler Garner ] - * [d03e07] Fix AttributeError in the ogc_server_settings configuration check. - - [ Simone Dalmasso ] - * [83cf4f] further work towards 14.04 - * [1edb6e] make flake8 happy - * [4008bf] fix apache sites on 14.04 and add arrest package - * [49b6a7] fix the updatemaplayer ip location - * [64997d] use the ogc_server internal location to create the thumbnail but save the link with the public location - - -- Simone Dalmasso Wed, 23 Jul 2014 09:42:50 +0000 - -geonode (2.4.0+alpha2) trusty; urgency=high - - * [0960df] updates to support ubuntu 14.04 - - -- Simone Dalmasso Mon, 21 Jul 2014 09:39:06 +0000 - -geonode (2.4.0+alpha1) trusty; urgency=high - - [ mwengren ] - * [13dc6c] Modified resource retrieval from gs_catalog in layers/models.py and added assumption that store exists within workspace in updatelayers to improve speed. - - [ Micah Wengren ] - * [a24c9d] Updated code to obtain gs_resource in models(layers) to use gsconfig get_resource function with workspace and store passed. Fixed issue with gs_slurp and error handling for nonexistent workspaces - * [98b6c3] Remove some unnecessary debug lines - * [cb9745] Fixed issue with var reference names in get_resource() call in Layer.store_type property function - - [ Simone Dalmasso ] - * [ebed3f] make it working with django 1.6 - * [afe45e] initial work towards django 1.6 - - [ Tyler Garner ] - * [8873a4] Fix the importer upload. Fixes #1312. - - [ Jeffrey Johnson ] - * [d58dc5] Initial add Groups redux - * [15b749] geonode group security redux - - [ Tyler Garner ] - * [093087] Clean up the groups tests. - * [f63d69] Use is instance vs type when determining class type of an object. - - [ Jeffrey Johnson ] - * [514532] Check group permissions in layer_acls - * [a8279d] Check groups in search perms - * [18b96c] Fix import in security/auth.py - * [108a1a] Commenting out GroupLayer and GroupMap Foreign keys due to circular imports - * [81a80a] use geonode.contrib.groups.models.Group vs geonode.contrib.auth.models.Group in layers/views.py - * [1f39f9] use geonode.contrib.groups.models.Group vs geonode.contrib.auth.models.Group in search/search.py - * [c03c64] use geonode.contrib.groups.models.Group vs geonode.contrib.auth.models.Group in security/auth.py - * [c98b13] use geonode.contrib.groups.models.Group vs geonode.contrib.auth.models.Group in security/models.py - - [ Tyler Garner ] - * [911858] Add a groups m2m field on the ResourceBase. - - [ Jeffrey Johnson ] - * [e548d8] Initial support for groups in permissions widget. Return groups in geonode.views.ajax_lookup - - [ Tyler Garner ] - * [ffec0e] Check a user's group permissions in objects_with_perm. - * [4764b4] Add a post_save hook to create default permissions on new resource base objects. - - [ Jeffrey Johnson ] - * [c9897e] add set_default_permissions and set_permissions to PermissionLevelImixin - * [d9afb3] use group.slug in ajax_lookup - * [4f7ca4] Move to using set_default_permissions and set_permissions from PermissionLevelMixin in Layers, Maps, Documents - * [f264de] Move permissions views to geonode/security/views.py - * [fc570e] Fix imports - * [3405e7] Fix security tests to use new setup - - [ Tyler Garner ] - * [75a177] Add signal to automatically set group permissions when groups are added to an object. - * [76ec50] Fix a missing import. - * [5ae168] Fix the group select box on the upload form. - * [3e74e6] Populate groups in permissions input element. - - [ Simone Dalmasso ] - * [9ca37f] basic rest api setting - - [ Tyler Garner ] - * [fd8f92] Delete groups when they do not exist in the perm_spec. - - [ Jeffrey Johnson ] - * [ce9873] Initial add geonode.contrib.services (from previous work of mine and worldmap) - * [04705a] Removing migrations - * [4df111] Initial work to make geonode.contrib.services work with 2.0 structure - - [ Simone Dalmasso ] - * [295deb] add user api - - [ Jeffrey Johnson ] - * [7f7d54] Update services templates - * [6a7933] Update srid and bbox fields in services - * [8bcfb0] Add service to Layer model - * [85c6b4] Add services.service to ACTSTREAM_SETTINGS and add USE_QUEUE - * [44cb5b] Fix services tests - * [eb3690] Add services_base template - * [b893c5] comment out activity.send while testing services - * [a4e243] Fix url refs in service_detail.html - * [4d33a9] Add settings for cascaded workspaces - * [077ce4] More work in services/views.py (force cascaded for now for testing) - * [0daf5d] Adding geonode/contrib/__init__.py - - [ Simone Dalmasso ] - * [eb45a8] add basic granular authorization - - [ Tyler Garner ] - * [d2ab70] Remove the groups m2m field from resource base and clean up the group detail page. - * [e2fd44] Remove the add_layers/add_maps functionality from the groups app. - - [ Jeffrey Johnson ] - * [e261a4] Remove print statements, convert layer.id to string and fix bbox refs - - [ Tyler Garner ] - * [815ddf] Remove the crispy-forms dependency. - * [225c57] Improvements to the group update and create views. - * [317034] Add the Groups list view and tie it into geocode.search. - * [70f20e] Include static file changes. - * [8086dd] Improvements to the group list view. - * [23419b] Add the last_modified column to the group_test_data fixture. - * [b78763] Fix typo causing group detail context to be saved as a tuple. - * [2bf441] Add the group remove route. - * [c2d505] Remove hardcoded slug from the groups list item template. - - [ Jeffrey Johnson ] - * [52f149] Fix indexed WMS so it works in the Layer Detail page - - [ Tyler Garner ] - * [87e15f] Fix for failing tests. - - [ Jeffrey Johnson ] - * [f61732] Switch to using layer.ows_url - - [ Tyler Garner ] - * [a73667] Add the group_remove template to git. - - [ Jeffrey Johnson ] - * [f6bde6] Minimal implementation of group membership management - * [808dcc] Make AGS layers work in the Map Viewer - - [ Tyler Garner ] - * [d7874d] Fix the upload Javascript to forward the window to the next upload step when appropriate. - * [73610d] Reset the _create_time_form to use mostly the old code. - - [ Simone Dalmasso ] - * [ed7d4c] make authorization apply the permissions based on the object type - * [a81984] use the correct object in read list - - [ Tyler Garner ] - * [2befb8] Highlight the group navbar item when the user is on the group detail page. - - [ Jeffrey Johnson ] - * [e62713] Add OGP_URL to settings.py - * [85fce8] Initial work to make groups optional - * [6a36ea] Fix use of OGC_SERVER location - * [a153c4] Handle for local layers without a service - * [cecd8c] Reuse select2 stuff from permission form in group_members.html - - [ Tyler Garner ] - * [a4eb92] Add a migration for the groups models. - * [007d09] Fix breaking test case. - - [ Jeffrey Johnson ] - * [017409] fix ptype references - - [ capooti ] - * [dde455] Added migrations for security groups stuff. - * [e96cb4] Added services in template if it is installed. - * [b5900f] Added a link to register a new service. - * [8d8c92] Removed service relation in layer, now we have a m-m relation via ServiceLayer. - - [ Simone Dalmasso ] - * [fa77ae] split installed_apps and run tests on geonode_apps - - [ Jeffrey Johnson ] - * [f28150] Fix a few things after applying @capooti's patch to use m2m for Service<->Layer - * [323c1c] Make services optional - * [a47701] Only include services urls if the module is enabled - * [678d8e] Switch to GEONODE_APPS method for running tests - - [ Tyler Garner ] - * [55c136] Safely handle activities with non-actionable parameters. - - [ Jeffrey Johnson ] - * [309a23] Add arcrest to install_requires - * [6f1222] Fix merge problem - - [ Simone Dalmasso ] - * [b9b356] preserve the query set in authorization - * [ada214] add faceting to meta - * [98e2d5] add filtering in common meta - * [9e9394] add resource base api - * [5dd42c] make ResourceBase a polymorphic model - * [159cd8] add django-polymorphic to setup.py - - [ Ariel Nunez ] - * [ab4b5b] Create admin user before attempting to load data in cloud scripts - - [ Simone Dalmasso ] - * [294ce1] api layer tests - * [df0fb8] add api to installed apps - * [332186] add keywords and category api and filtering - * [b23281] add owner filtering - * [943e0c] remove the default category logic as not used anymore - - [ Ariel Nunez ] - * [950bf6] Removed allow_external from paver setup, if anyone is having trouble installing pycsw, please add those flags - * [411c7a] Refactored document thumbnail handling to be flatter - - [ Simone Dalmasso ] - * [daa0bb] add unicode for thumbnails - - [ Ariel Nunez ] - * [4abcc5] Disable the share tab if SOCIAL_BUTTONS is False - - [ capooti ] - * [50c860] Removed the leaflet config setting, not being used anymore. - - [ Simone Dalmasso ] - * [e8ba77] add date filtering - - [ Jean Jordaan ] - * [343a5f] Small fixes while reading - - [ Ariel Nunez ] - * [9bc556] Fix failing tests in documents app - * [0efb98] Removed line with partial image string - * [7982a4] Fix failing document tests - * [b98935] Properly handle redirects in proxy - * [f83344] Removed pinax theme account context processor. - * [fd3077] DRYed proxy's tests - - [ capooti ] - * [2e2b74] Added a way to debug geoexplorer. Instructions included. Thanks @ingenieroariel. - - [ Ariel Nunez ] - * [3ef9b3] Layer detail pages is n times faster, where n is the number of layers in your geonode - * [f48b14] Added Evan Ricafort to AUTHORS (XSS vulnerability). Thanks! - - [ Simone Dalmasso ] - * [06bab4] Update the complete install guide to manual install guide - * [343bde] better handle faceting - - [ Ariel Nunez ] - * [ff5e28] Do not use a requirements.txt file and call gsimporter from pypi - * [c1013f] Enabled integration and csw tests in travis - * [94a0a8] Add geoserver setup back to travis tests - * [4ccc1f] Email the developers list if the tests are broken - * [043959] Fixed merge conflict - * [3f41f9] remove - * [fb7fae] Fixed conflicts in groups and services integration - * [7934ea] Removed print statement in services test - * [630332] Added dependency on geonode-arcrest - * [690758] Bumped gsconfig to 0.6.8 - - [ Tyler Garner ] - * [5733bc] Allow user to upload external documents. - * [e8a4c3] Remove the base.css file from the less directory. - * [09213c] Explicitly add the event parameter to the doGeoGitToggle function. Fixes error in Firefox. - - [ Simone Dalmasso ] - * [062964] fixes #117 - - [ Ariel Nunez ] - * [053e4b] Login and Logout do not redirect to homepage. - * [50d35c] Added correct link to WMS GetCaps in layer group publishing, fixes #141 - - [ Simone Dalmasso ] - * [09cf28] Better errors handling in upload js - - [ Ariel Nunez ] - * [a14ccf] Better support for different types of errors in javascript upload code - * [969214] First steps is now easier to find, and complete install is buried down so it is harder to find - * [3c4b6b] Use the readthedocs theme - * [eca748] Added section where to put all the links to make it easier to find the right documentation entry the user wants - - [ Matt Bertrand ] - * [4e4660] Integrate previous work on haystack/elasticsearch into current search/explore UI - * [452433] Adjust boost values for title, abstract - - [ Simone Dalmasso ] - * [09abc9] fix integration test - - [ Paolo Pasquali ] - * [ae1534] Fix Group logo image style - - [ Ariel Nunez ] - * [41c233] Removed read the docs theme - - [ Tom Kralidis ] - * [a44046] Update architecture.txt - - [ Matt Bertrand ] - * [2667a7] Remove 'iid' or rename to 'id', tweak search_indexes - - [ Mark Iliffe ] - * [aa0c34] Followed the MacOSX instructions for geonode, corrected the documentation in the README file to include missing steps. These ammounted to ensuring pip installs all dependencies and ensuring pillow is installed. - * [24dd88] Removed whitespace - - [ capooti ] - * [21193e] This fixes #1026. - - [ Simone Dalmasso ] - * [d893f2] fixes #640 - * [7b5e5a] fixes #779 - - [ capooti ] - * [23ee1b] This fixes #1430. - - [ Simone Dalmasso ] - * [35ebd4] fixes #1306 - - [ capooti ] - * [66b89e] Fixes #1000. - - [ Ariel Nunez ] - * [852ed4] Added Mark Iliffe to the AUTHORS list - - [ gamesbook ] - * [0d63cc] Update README - - [ Matteo Nastasi ] - * [db6b8d] Fix bug #1434 - - [ Mark Iliffe ] - * [b9e555] Integrated the comments and suggestions of ticket #428 (https://github.com/GeoNode/geonode/issues/428) - - [ Paolo Pasquali ] - * [5dd389] One row header - - [ capooti ] - * [e0fe9c] fixes #637 - - [ Mark Iliffe ] - * [ebac65] Have rewritten the introduction for the installation documentation to better reflect the actuality of how to install Geonode. Also included line by line information how to install for the various platforms. - - [ sbsimo ] - * [8b8f8a] added Join Group button for joining public groups - - [ Mark Iliffe ] - * [b07e3d] Added virtualbox instructions - * [b76e50] VMware instructions. - * [a397e6] References the quick installation docs - * [16f0b5] Referenced the new name for custom installation - * [f569b3] Referenced the new name for custom installation - - [ Simone Dalmasso ] - * [14af18] bump to gsconfig 0.6.9 - - [ Matt Bertrand ] - * [df8b73] Make haystack/elasticsearch optional - - [ Mark Iliffe ] - * [2cf012] Added documentation to facilitate the sharing of maps. - - [ Matt Bertrand ] - * [5e8649] Redirect search API calls to Haystack if activated - - [ Tyler Garner ] - * [5e30f7] Minor improvements to the sharing documentation. - - [ Yewondwossen Assefa ] - * [f22dcc] Update production.txt - * [5f9e7a] Update production.txt - * [e893f5] Update production.txt - - [ Ariel Núñez ] - * [b7dbdf] Added new authors. - - [ Simone Dalmasso ] - * [e53128] added Simone Balbo to the list of contributors - - [ capooti ] - * [cb013a] Moved South to latest version. - - [ Simone Dalmasso ] - * [84eecf] refactor and fix the permissions management - - [ capooti ] - * [2480d6] Resetting migrations from 2.0 tagged version. - - [ Simone Dalmasso ] - * [d1ec5e] logout on home page and put back the login to the same location - - [ Mark Iliffe ] - * [953f5c] Added a development roadmap info. - - [ Simone Dalmasso ] - * [0d0b95] distinct objects and add search tests - * [162c7d] add polymorphic migration - - [ capooti ] - * [f83e57] Remove the edit permissions widget link from menu and not only from righ side bar. - - [ Simone Dalmasso ] - * [28cf05] move the rating delete to the pre_delete layer signal - - [ Ariel Nunez ] - * [57ed8c] OGC_SERVER setting can now be an empty dictionary - - [ Jean Jordaan ] - * [db34a1] Fix whitespace - - [ Daniel Dufour ] - * [70b587] autofill doc title on upload - - [ Tyler Garner ] - * [068bff] Add the USE_DOCUMENTS context back to the resource_urls context processor. - - [ Ariel Nunez ] - * [0f3395] Removed conditional imports in search_indexes. - * [b4bece] Simplified thumbnail handling for layers - * [7fc30f] Fixed missing import in proxy module - - [ Bart van den Eijnden ] - * [68df8d] Update geoexplorer.txt - * [0edd85] Update geoexplorer.txt - - [ Tyler Garner ] - * [a76245] Safely check for settings.HAYSTACK_SEARCH and small PEP 8 improvements. - - [ Ariel Nunez ] - * [00c75f] Removed unused imports - * [3254b4] Fixed import in geoserver signals - - [ Tyler Garner ] - * [3ea260] Fix one last check for settings.HAYSTACK_SEARCH. - * [7a5e8d] Use the django static files finder to locate the document placeholder thumbnails. Fixes #1447. - - [ Ariel Nunez ] - * [daecfa] Fixes #1450 - - [ capooti ] - * [642c15] Formatted properly some doc pages in order to make it working properly with RTD theme. - * [74cb89] Hack to avoid RTD to be confused, as per sphinx_rtd_theme documentation. - - [ Ariel Nunez ] - * [eebe17] Moved geoserver upload out of save method - * [b1a1ab] Moved management commands to relevant apps. - * [7172ab] Do not ignore changes in geonode.geoserver - * [674264] Created geoserver/context_processors - - [ Tyler Garner ] - * [9b2df4] Remove the new layer verification since the verify method no longer exists. - - [ Jean Jordaan ] - * [9b5114] Tidying while reading - - [ Simone Dalmasso ] - * [b9081a] small update to the install doc - * [9772e9] remove unwanted comma, was breaking the upload - * [e25700] remove api from installed apps - - [ Ariel Nunez ] - * [0fc037] Moved geoserver specific code to geonode.geoserver - - [ capooti ] - * [db91c8] Readded the GeoServer section in the migration doc (not sure why it was removed). - - [ Ariel Nunez ] - * [a3b5c7] Fix refs to geonode.utils.ogc_server_settings - - [ Tyler Garner ] - * [a44880] Import the Layer model into geonode.geoserver.signals. - * [a978dc] Add missing imports to the Geoserver views. - * [584228] Bump Django. - - [ Ariel Nunez ] - * [c5390e] Include uploaded file in paver reset - * [79631e] Simplified handling of thumbnails. - * [314387] Fixed map saving. - * [782a9c] Missing imports - - [ Simone Dalmasso ] - * [f5e23b] first work towards client api search - * [bf73de] dry it and activate for maps docs and layers - * [439a27] remove unwanted - - [ Tyler Garner ] - * [2832fc] Pass in the style name as a string vs an object when settings styles on layers. Fixes #1456. - * [a78ec4] Fix typo'postigs' - - [ Ariel Nunez ] - * [0228d0] Fixes #1458 - - [ Jean Jordaan ] - * [5cb6b9] Some more doc tweaks - - [ Simone Dalmasso ] - * [9912c3] refactor and add categories counts - * [87f0cd] add tags counts - * [b061e6] update faceting - * [0adc4e] update faceting - * [171d23] simplify faceting - * [8a65d9] add type filtering counts - - [ Tyler Garner ] - * [0b4983] Small fixes to thumbnails. - * [6ba07c] Refactor the last commit to fix broken tests. - * [c98b7d] Moves geoserver-specific url routes to the geoserver app. - * [daeac6] Remove the duplicate forward slash from the gs/rest/layers url route. Fixes #1457. - - [ Jean Jordaan ] - * [f4f80a] Tidying while reading - * [74f73c] Tidying while reading - * [e24cd5] Some more doc tweaks - * [7ce377] Tidying while reading - - [ Simone Dalmasso ] - * [a82a1d] add multiple choice selectors - * [a5dc79] add search - * [0474d0] properly name the filter - * [5d6f40] simplify faceting - - [ Tyler Garner ] - * [fab5e7] The resolve_user function returns the user's full name and email regardless of authentication method. - - [ Ariel Nunez ] - * [e881e6] Removed resourcebase signals - - [ Tyler Garner ] - * [f56516] Update the body element's padding-top value to reflect the smaller navbar. - * [ffaa4f] Fix the spacing between buttons on the Document detail page. Fixes #1451. - - [ Ariel Nunez ] - * [cffc51] Fixed maplayer saving in models - * [f6cdfe] Fixed failing map tests - * [0b21f0] Add a threshold to identify bounding boxes too tiny for thumbnailing - - [ Tyler Garner ] - * [88e410] Add a signal to execute the set_missing_info method when Documents are saved. - - [ nathanhilbert ] - * [de805e] Add Link from document to thumbnail - - [ Stefano Menegon ] - * [e179d5] Add missing layers' sources in map composer - - [ Ariel Nunez ] - * [020bcc] Use the map bbox in the thumbnail link generation - - [ Simone Dalmasso ] - * [786a77] add faceting on categories and keywords - * [6b8869] use category faceting - - [ Ariel Nunez ] - * [8204ae] More explicit handling of bounds setting - * [f9cb22] Added FIXME to tests that depend on a network connection - * [ffbcfb] Moved methods for saving bbox, center and zoom to resource base - * [af5139] Removed get_extent method in map in favor of map.bbox - * [26849f] Make sure the bbox parameter is set in the map creation - - [ Jean Jordaan ] - * [ea9a75] Make sure that MAP_BASELAYERS is current - * [672e59] No reason to make the final settings non-overridable - - [ Simone Dalmasso ] - * [80756a] only show keywords with count > 0 - - [ Ariel Nunez ] - * [96d4d0] Use intermediate LayerFile model during upload. - * [86d3ca] Fixed keywords saving - * [966e58] Show UploadSession in admin - * [9becca] Moved layer_acls test to geonode/geoserver/tests - * [1d73ba] Group applications related to geoserver - * [107239] Set a default for the typename - - [ Simone Dalmasso ] - * [f166a1] respect security in categories and keywords facets - * [8b2455] add server side resources faceting with security - * [659e30] just facet the needed type - - [ Ariel Nunez ] - * [a2b2d9] Added get_base_file method to Layer - * [bd2130] Fixed references to typename in tests. - - [ Simone Dalmasso ] - * [63774d] add filter by type - - [ Tyler Garner ] - * [5dacd6] Add missing imports to geonode/geoserver/helpers.py - * [442978] Safely ensure indexes exist before lookups when building the attribute map. - - [ Simone Dalmasso ] - * [881244] add filter type also for layers - * [954e4d] respect the initial url in the results - * [46869e] unify search_content - * [c05ec2] add search_content - * [aab8b9] remove unneeded tags - * [54db2b] default order by date descending - - [ Tyler Garner ] - * [854021] Prevent the cascade_delete geoserver helper from deleting entire GeoGIT stores. - - [ Simone Dalmasso ] - * [67c868] add sorting - * [26547f] activate filters for categories and keywords based on the input url - * [8a5e9f] make sure the lists show the filters passed in the url - * [1d1c91] add pagination - * [d8063d] make pagination working - * [0d6b29] better handling of pagination - * [3012b9] remove the pagination limit for api and manage it client side - * [a5b2a1] remove unwanted comma - - [ Tyler Garner ] - * [f5dd38] Add missing imports in the Geoserver app. - * [b9cc63] Add advanced search form link back to the navbar. Fixes #1468. - - [ Simone Dalmasso ] - * [64f7a1] remove the advanced search form - * [f8683c] add popular and share counts to base and allow ordering on them - - [ Jean Jordaan ] - * [57c090] Fix thumbnail URL - - [ Simone Dalmasso ] - * [4393fc] add angular to assets and turn off debug static - * [7b0f9f] some cleanup - * [c2390b] fix missing tpl in index - * [7fefe6] profile list now uses apis - * [976409] fix avatar url - * [0c3ee6] use ng-src to load the avatar - - [ state-hiu ] - * [0ce2d7] Initial License Enhancement (Including Detail UI and Metadata. No searching). - - [ Jeffrey Johnson ] - * [e662d8] Fix after merge and import - * [e5dbbf] Fix import for set_attributes - - [ Ariel Nunez ] - * [30665f] Better error reporting on failures adding remote arcgis services - - [ Simone Dalmasso ] - * [f9d4bd] add sorting for users - * [35a3d1] small fixes - - [ Ariel Nunez ] - * [62d9bd] Removed nested try/except blocks in favor of flatter code - - [ Simone Dalmasso ] - * [1755fd] use template view in lists and remove the tag view - - [ Ariel Nunez ] - * [290185] Added thumbnails to remote arcgis server. - * [bddf57] Added legend as a link - * [98213b] Added remote services link in admin dropdown - * [e7b401] Added buttons for upload, help and remote services - * [b4fc2b] Added legend creation in arcgis services - * [1bc9ae] Avoid sending data to facebook and google in layer detail pages. - - [ Simone Dalmasso ] - * [fbb2fd] adds group api and group list/management through them - - [ Ariel Nunez ] - * [02faad] Implemented legend from arcgis services - - [ Simone Dalmasso ] - * [2eded8] add spatial search in api - * [879cf7] fix typo - * [1d5df9] just call the categories and keywords api when needed - - [ Ariel Nunez ] - * [edccfc] Added kmz download - - [ Simone Dalmasso ] - * [430143] added frontend spatial search with leaflet - * [c72fe0] fix gitignore - * [952fb5] clear the spatial search - * [0d968d] get rid of select2 for searching - - [ Ariel Nunez ] - * [e1fc96] Improved bbox behavoir in new map for corner cases - * [77525d] Save bbox in wgs84 for remote services - * [a974f7] Do not use --all on syncdb - - [ Simone Dalmasso ] - * [20e3f0] always load the leaflet directive - * [65b91c] remove zoom control for search map - - [ Jean Jordaan ] - * [dd3309] Juggle versions to make everyone happy - - [ Ariel Nunez ] - * [752d46] Pegging pinax-theme-bootstrap-account to the latest pypi release - - [ capooti ] - * [745340] Added a link to the invite user page. - - [ Simone Dalmasso ] - * [44e6e8] add leaflet directive to assets.js - - [ Ariel Nunez ] - * [e9cb6c] Removed migrations from master. - - [ Paolo Pasquali ] - * [fc7054] Merge branch 'api-search' of https://github.com/simod/geonode - * [9bf4bf] Fix home layout - - [ Ariel Nunez ] - * [bfd99d] Pegged setup.py to last working versions of owslib and pycsw - - [ Paolo Pasquali ] - * [d2d5d8] Initial layout fix - - [ Simone Dalmasso ] - * [936126] make unit tests to pass - - [ Tom Kralidis ] - * [87c90d] update tests - - [ Simone Dalmasso ] - * [029c5a] don't put empty keywords when loading layers - * [5a2e93] use stamen base layer in extent search - * [9163eb] change attribution - - [ Ariel Nunez ] - * [d72daf] Improved bbox calculation - * [f47bad] Call saving only once and get bbox information using gdal - * [3b9f05] Set center, zoom and bbox string from bbox_x0 and friends - - [ Paolo Pasquali ] - * [c409c4] Some layout fixes - * [58e639] Restore extent filter style - - [ capooti ] - * [1ce32b] Now it is possible to edit the profile for current user without passing the username in the url (needed by geonode-user-accounts). - - [ Ariel Nunez ] - * [966a73] Fixed metadata saving - - [ Tom Kralidis ] - * [4a5b11] remove unused function - * [3c32f1] safeguard wkt setter - - [ Paolo Pasquali ] - * [ed89bc] Add some style in home page - * [c5ffc2] Style fixes - - [ Simone Dalmasso ] - * [5c7a67] fixed typo - * [cb6b2b] add thumbnail url and date picker - - [ Paolo Pasquali ] - * [91eaca] Fix nav filters style - - [ Simone Dalmasso ] - * [ffdb1a] add date search - - [ Paolo Pasquali ] - * [67d089] Fix bower, grunt - * [929c1f] Fix font-awesome css links - * [c98d97] Fix templates style - - [ Ariel Nunez ] - * [517803] Remove README from static folder and add Makefile instead. - * [3895e1] Added static handling to paver setup - - [ Paolo Pasquali ] - * [759416] Fix some templates - * [54c95b] Fix some layout - * [4768ab] Fix header in maps view - - [ Ariel Nunez ] - * [a758a2] Layer now keeps track of upload_session - * [e23d15] Updated paver documentation - - [ Paolo Pasquali ] - * [4137e1] Disable bootstrap responsiveness - * [df275d] Add some content in index - * [54f88d] Some layout fixes - * [096c5a] Add menu style - - [ capooti ] - * [5ba59b] Bump geonode-user-accounts - - [ Ariel Nunez ] - * [2ce21f] Added python-gdal to .travis.yml - * [efef82] Added python-gdal to README - - [ Paolo Pasquali ] - * [13ec7d] Index layout - * [39e291] Base layout fix - - [ Simone Dalmasso ] - * [dc3187] update and clean the js - * [fd5308] some css polishing - * [8fb743] assets.css goes first - * [19ac43] activate search in header - - [ Paolo Pasquali ] - * [03b420] Add date to resourcebase_snippet - - [ Simone Dalmasso ] - * [cdfb43] don't use jquery deprecated method - * [3b98a7] clean 500 template - * [6e097d] delete unneeded file - * [990690] add back django pagination - * [855ec8] update the 500 template - * [c6df99] remove docs migrations - * [8bd58e] remove the view by list/grid - * [619aed] no more search in home page - * [ed968f] don't list the profiles twice - * [11e445] don't add the limit and offset in url unless they are specified by the user - - [ Paolo Pasquali ] - * [f8bc8e] Some resource_base_snippet style - - [ Simone Dalmasso ] - * [6e220a] return full categories and owner in apis - * [e90a6d] revert the search limit and offset in search pages. we need that - * [5f27f8] add featured field in resource base, add featured api, add featured logic to the homepage - - [ Ariel Nunez ] - * [2506f5] Added geonode.contrib.dynamic - * [7d0aee] Added accessor fields to layer - * [eba6ee] Commented out dynamic by default - - [ Simone Dalmasso ] - * [b63595] distinct on resources and try again to put the api in installed apps so the tests are ran - - [ Tyler Garner ] - * [8a4063] Allow users create GeoGit repositories with underscores in the name. - * [b38020] Remove apt-get step from the OSX instructions. - - [ Ariel Núñez ] - * [ffed42] Update README - * [fc257b] Update README - * [286044] Update pavement.py - - [ Simone Dalmasso ] - * [154250] fix url imports and hashlib for django 1.6 - - [ Paolo Pasquali ] - * [5d409e] Some layout fixes - - [ Simone Dalmasso ] - * [497a19] add default value to map layer transparent - - [ Paolo Pasquali ] - * [cfa2e8] Add fake links to items list actions - - [ Simone Dalmasso ] - * [baac23] fixed proxy tests, thanks @ingenieroariel - * [39aa98] add api url - * [4dd867] add api url - * [ab7235] save the layers even if the was is not available, by @ingenieroariel - - [ Paolo Pasquali ] - * [8d5836] Fix some layer detail style - - [ Simone Dalmasso ] - * [41f1ea] ad a wcs links test to integration - * [1cb118] update setup.py to geonode django1.6 forks - * [f6f977] put a help text for the featured field - - [ Ariel Nunez ] - * [49d4ce] setup.py now supports Django from 1.6.1 to 1.6.5 - - [ Jean Jordaan ] - * [f87cbb] One name is enough for one thing - - [ Simone Dalmasso ] - * [2bec65] better facet handling through a template tag - * [06d2f7] add faceting template tag in layer list - * [8b6467] don't use try except if there's no need as Ariel suggests - - [ Jean Jordaan ] - * [dbcf13] Fix typo - - [ Ariel Nunez ] - * [f20c2f] Preparation of setup.py for release - - [ Simone Dalmasso ] - * [e5db50] add back services in header - - [ Paolo Pasquali ] - * [2e01fd] Fixing Upload Layer page layout and other minor style fixes - - [ Simone Dalmasso ] - * [6c7e29] let's keep the services in the user dropdown, apologize for adding and removing this - - [ Paolo Pasquali ] - * [d4f5c7] Upgrade font awesome to version 4.1 - * [8472f5] Fixes font folder name according to font-awesome css - - [ Ariel Nunez ] - * [3814af] Replaced placeholder text - * [fe339e] Added users and layers count to facets for homepage use - * [8b0d04] Fix facets for layer page - * [b11582] Added count to homepage - - [ Simone Dalmasso ] - * [29b5c9] fix the ratings - - [ Ariel Nunez ] - * [2e390b] Committed outputs from paver static - - [ Simone Dalmasso ] - * [baeb1f] avoid showing users faceting in search - * [cc9b6a] initial work towards permissions on resourcebase - - [ Paolo Pasquali ] - * [65e2f9] Improvements in map detail, layer detail layout. Fix document upload layout. - * [b3c5a4] Fix Documents Upload extended template - * [3d4627] Adding id to upload button (for styling purposes) - - [ Simone Dalmasso ] - * [850aac] make upload working - - [ Paolo Pasquali ] - * [09cc6e] Fix permission modal in layer detail - - [ Tim Welch ] - * [66439f] Instruction fix - - [ Simone Dalmasso ] - * [5c9d6b] make layers tests to pass - * [6dd9d2] fix maps, docs and api tests - - [ Ariel Nunez ] - * [e72089] Upgraded pycsw to latest - * [81db05] An UploadError is acceptable when a layer does not have a good projection - - [ Paolo Pasquali ] - * [e1581e] Fix doc detail and info panel layout - * [959576] Fix dl-horizontal dt text-overflow - - [ Tim Welch ] - * [453930] Added port to instruction - - [ Paolo Pasquali ] - * [4a20e2] Fixes Filters active colors - - [ Simone Dalmasso ] - * [32484f] some fixes and dropped the generic role mappings tables - * [c03961] add guardian to setup - - [ Paolo Pasquali ] - * [79a896] Improvements in profile detail layout - * [2771a3] Move my activities link in profile detail - - [ Ariel Nunez ] - * [343508] Disable temporarily CSW tests - - [ Jean Jordaan ] - * [7f7d9d] Change to PRINT_NG, check for PRINTNG - - [ Paolo Pasquali ] - * [3c8ebb] Improvements in Groups and profile list layouts - - [ Ariel Nunez ] - * [eabadd] Added transparency to overlay in leaflet - * [c56f7b] Fixed integration tests in my local box - - [ Jean Jordaan ] - * [cc732e] Add second location to be updated - - [ Ariel Nunez ] - * [967afa] Closing the db connection is not needed in Django 1.6 - * [a17ac9] Added bower install to static makefile - * [31fae2] More results per page by default - * [48c5ad] Exclude csw xml fields from the API - * [331ef8] Fixed typo in index page - - [ Simone Dalmasso ] - * [171077] use gravatar if no avatars are set in the profiles api - * [bcaa2f] add back the edit metadata link - * [23ce56] test default non polymorphic - * [b5bd34] add polymorphic queryset to manager - * [53aa96] make use of the polymorphic model only when needed - * [3ea1d9] remove unwanted meta - - [ Ariel Nunez ] - * [7af012] Upgraded bower.json - * [5ebba3] Added resolutions to bower.json - * [f48007] Added a better resolution for jquery - * [efbb49] Another try at jquery resolutions - * [f60d7c] Upgraded datatables - * [f290f0] Updated static files - - [ Simone Dalmasso ] - * [be7567] remove the default topic category - * [a30f3e] fix api categories counts - * [1c7c90] fix categories filter - - [ Tyler Garner ] - * [4cb7c4] Fix #1472. ACLS list is incorrect for layers assigned to a public group. - - [ Ariel Núñez ] - * [e51302] Run the tests in the geoserver task. - * [902f3d] Peg geoserver.war to the 2.4-SNAPSHOT - - [ Paolo Corti ] - * [470198] Disabled test_register_csw - * [3b7a5e] Disabled test_register_csw - * [0fa8c3] Enabled latest geoserver to 2.5 - * [7f94d4] Moved django-geoexplorer 4.0.3 with classify - - [ Matt Bertrand ] - * [c21d75] Add proxy url for geoserver sldService module; add additional gxp files for classifier - - [ capooti ] - * [d16326] Removed paver static from paver setup but added it to the travis.yml so it is tested - * [f9909e] Updated setup.py to use django-geoexplorer 4.0.4 - - [ Ariel Nunez ] - * [25e0dc] Let paver static install node in travis - * [7c9c75] grunt-cli was missing from package.json - - [ Simone Dalmasso ] - * [b8641b] link geonode groups to django groups - * [69b3d2] remove the role mappings logic - * [d89c6b] add basic set_default_permissions - - [ Ariel Nunez ] - * [20027f] Re-enabled csw tests - * [ec0f6e] Bumped owslib and pycsw - - [ Jeffrey Johnson ] - * [53ff83] Initial work on hooking up haystack - * [77c700] Hook up new search properly - * [6ddab4] Hook up the query properly - * [f39a75] more work on search - * [42fb51] Hook up paging - - [ Tom Kralidis ] - * [465782] safeguard non-null for language - - [ Tyler Garner ] - * [dd2424] Check a datastore for additional resources before deleting. Fixes #1510. - * [d05380] Remove multiple if statements in the geoserver cascading delete logic. - - [ Matthew Hanson ] - * [d3297a] updatelayers updates bbox, fixes #1514 - - [ Ariel Nunez ] - * [6ac947] Attempt to fix travis build error - * [93bebf] Automatically accept the python-gdal installation - * [cee015] python-gdal - * [ea027f] Added libgdal1h - * [826c34] Added libspatialite - * [993574] Borrowing python-gdal install line from perrygeo/python-raster-stats - * [2cf024] Still working on fix for Travis - * [341bcc] Added comment about travis problem - * [f63ed9] Added dev list back to travis - * [b3202f] Added libjai-imageio-core-java to see if geoserver stack trace error disappears - - [ Simone Dalmasso ] - * [27abc4] create a get resource method - * [02f2a1] use the guardian tables to check the if there are permissions - * [158df5] correctly check for the resourcebase_ptr - - [ Brylie Oxley ] - * [a709cb] Additional installation steps. - - [ Simone Dalmasso ] - * [a3fffa] fix the layers_acls - * [8f9316] use guardian to get permission info - - [ Tim Welch ] - * [671a09] add exception workaround - - [ Simone Dalmasso ] - * [8d9d97] use resource in layer detail - * [54c6c0] change the permission form and the set permissions function - * [c52309] use AnonymousUser instead of anonymous - * [f61cbb] use resource instead of layer - * [0d6f73] use the guardian get_objects_for_user to filter permissions - * [cb62a4] make layers test to pass - * [23fc93] refactor of the api security including the anonymous user check - * [2cf2a2] fix api and geoserver tests - * [a315ff] move security filters to security app - * [a5f782] first work to make groups inherit from django groups - - [ Jeffrey Johnson ] - * [5e652e] Work on faceting - * [dbdd1d] Include facets in json response, remove metadata_xml and csw_anytext from output - * [b9502d] Change base fixtures to have auto-assigned IDs - * [8f40ff] Initial work to add django-mptt - * [c51c99] Add django-mptt to setup.py - * [3e3284] Remove some errant chars - * [10aa27] Hook up Regions Display in Maps and Documents - * [9b20a8] Initial work to hook up autocomplete - * [40e184] ucomment in map_detail.html - - [ Tyler Garner ] - * [091d22] The member_remove route regex allows non-word characters. - - [ Simone Dalmasso ] - * [b62eb0] explicitly assign permissions to users - - [ Ariel Núñez ] - * [92ac59] Travis fixes - - [ Jeffrey Johnson ] - * [67ecb1] Initial work on porting MapSnapshot against master - * [a90baf] more work on custom and featured urls - * [c18df0] use looser matching in maps/urls.py - * [2e8822] Lookup maps by custom url if the mapid is not a number - * [6576fe] Use snapshot to lookup config if passed - - [ Matt Bertrand ] - * [5f4a8b] Work on services - - [ Ariel Núñez ] - * [2cdd33] Another attempt to fix build errors - * [08a11b] Do not prompt for authorization on add-apt-repo - * [d8a70a] Update .travis.yml - - [ Jeffrey Johnson ] - * [f5c5e2] Initial work to hook up autocomplete - * [b4b3f7] ucomment in map_detail.html - * [64930f] Show metadata help text form in bootstrap popup - - [ Ariel Núñez ] - * [3e524d] Added Mark and Jean to PRIMARY AUTHORS - - [ Simone Dalmasso ] - * [796add] move groups to core - - [ Jeffrey Johnson ] - * [e0971f] Return fixtures to using pre-assigned primary keys. - - [ Simone Dalmasso ] - * [0abbe1] first work towards profile as custom user model - - [ Jeffrey Johnson ] - * [1e0e1e] Hookup autocomplete in base search - - [ Simone Dalmasso ] - * [1208f1] make upload layers work - - [ Jeffrey Johnson ] - * [bb850c] Initial work to add model translations - * [88c413] Hook up forms to use TranslationModelForm - * [e35613] Hookup tabbed translation in admin for Layers, Maps, Documents - * [5f1aa3] Remove use of tabbed inputs and add admin to documents - * [d04dcb] Use base MediaTranslationModel - * [27f61b] Remove extraneous Media class from DocumentTranslationOptions - * [f70809] Add django-modeltranslation to setup.py - - [ Simone Dalmasso ] - * [20e09d] make layers, maps and documents to pass - - [ Jeffrey Johnson ] - * [bec0b5] Move MediaTranslationAdmin to a more sensible spot and dont hook up ResourceBase for translation directly (only do it with Layer, Map, Document etc) - - [ Matthew Hanson ] - * [a52a39] extent filter based on intersection rather than contains - - [ Jeffrey Johnson ] - * [9d3ba4] Handle for unmatched searches (dont update search bar) - * [291974] Submit search when selecting an autocomplete - - [ Simone Dalmasso ] - * [4659ac] make group tests to pass - - [ Matt Bertrand ] - * [9d36c0] make services a core module - * [66b876] Add ESRI-leaflet plugin, use lazy loading for layers - * [73d501] Fix layer lookup - - [ Simone Dalmasso ] - * [82da54] fix social and geoserver tests - * [3032f4] make services be resourcebase - * [fa11d0] remove unneeded perms levels - * [806ad9] make services a core app - * [122209] fix integration tests - * [b34d7f] add anonymous middleware - * [2f3d7c] remove the middleware as not valid and fix permissions check in templates - * [531c2e] fix profile snippets - - [ Jeffrey Johnson ] - * [7c622c] Disable autocomplete for topic category (the list is short enough) - - [ Simone Dalmasso ] - * [7ab022] added guardian to setup.py - * [91ff42] add temporary requirements file for dependencies - - [ Matt Bertrand ] - * [fb6e1d] Assign bounds of cascaded layers Set bbox to 4326 or web mercator depending on layer srid A few other bug fixes - - [ Tyler Garner ] - * [04d2f5] Fix the form on the create group template. Fixes #1525. - - [ Jeffrey Johnson ] - * [5ed37e] Show categories as a radio list with popovers - * [802696] Move CategoryForm to base/forms.py - * [2369f2] Hook up category help in Map metadata form - * [611a8f] Hook up category_help in documents - - [ Matt Bertrand ] - * [e68250] New assets.min.js including esri-leaflet - - [ Simone Dalmasso ] - * [d53f67] fix profile api - * [6658ef] fix autocomplete for profiles - * [93907a] make regions optional in metadata forms - * [69ae7b] put back esri-leaflet out of assets.js - * [6b170e] update requirements - - [ Ariel Nunez ] - * [164b6a] Regions should be allowed to be empty - - [ Simone Dalmasso ] - * [5e1075] remove unneeded security templates - * [bacd49] fix query error - - [ Jeffrey Johnson ] - * [c7faa0] Some changes to the People form and views now that we inherit from AbstractUser - * [0a98ca] Some fixup after changes to User and Group models - * [afff6d] Reconnect autocomplete - * [4f8822] Make haystack respect permissions - - [ Simone Dalmasso ] - * [c11acf] override tastypie to make it fast enough - * [3d9ddc] fix the base tags - * [3fd954] remove unused dehydrate fields - * [ede98c] better category and keywords count - - [ nathanhilbert ] - * [5f80fb] fixes issue 799-windows upload file locking - - [ Simone Dalmasso ] - * [f1b0a8] add denormalized fields for apis - * [bb3459] fix api for item detail - - [ Jeffrey Johnson ] - * [4da31a] Remove oid filter - - [ Simone Dalmasso ] - * [a0cb1c] try to fix it - * [a9bbbe] make serialization to work - - [ DHohls ] - * [d0e14f] Corrected default values for MODELTRANSLATION_LANGUAGES in settings - * [1f540c] Patch for issue #1528 - * [0cd592] Documentation patch for issue #1528 - - [ Matt Bertrand ] - * [24bd52] Add layer attributes to remote service layers Recommit changes to layer_detail page A few bug fixes - - [ Simone Dalmasso ] - * [1bbfa5] fix group list and group detail - - [ DHohls ] - * [067daf] Added upstream library required for psycopg in dev install document - - [ Simone Dalmasso ] - * [35d268] rename the activity template folder to actstream - * [37de04] don't overwrite the store name in updatelayers fixes #1512 - * [f3c6ca] exclude the group from groupProfile in admin - - [ DHohls ] - * [22f586] Added installation guide for GDAL in a virtual environment - - [ Matt Bertrand ] - * [1aa65c] Cache map and layer configurations on a per-user basis Disable/turn off maplayer records that user does not have permission to view - - [ DHohls ] - * [b6c0bd] Updated installation guide for GDAL - - [ Matt Bertrand ] - * [f52ddf] Handle local vs remote use case - - [ nathanhilbert ] - * [4766fa] fixes 1538 - - [ Jeffrey Johnson ] - * [0e3c0d] Hook up the filters properly so both orm filters and perms filters are working with haystack (NOTE: You need to up your index.query.bool.max_clause_count in your elasticsearch.yml to reflect the total number of layers you have in your system) - - [ Matt Bertrand ] - * [fad2d0] Some refactoring - * [efcdb6] Get rid of debug version - - [ Simone Dalmasso ] - * [dc67d0] fixes #1559 - - [ Tyler Garner ] - * [b51926] Fix the group update form. - - [ nathanhilbert ] - * [56b23a] The last file writing issue for Windows - * [ad4f13] Open uploaded files in binary mode on Windows. - - [ Simone Dalmasso ] - * [444951] don't count the anonymous user - * [f18e0f] delete the associated group on group profile delete - * [515aba] make service detail use guardian - * [b6ccae] fix remote store filter in api - - [ Jeffrey Johnson ] - * [1ea309] Put settings.py back to normal - * [357264] Change the search URL and id based on whether haystack is enabled or not - * [46a9b6] Remove haystack from INSTALLED_APPS by default - * [da5b6a] Whitespace cleanup - * [faba87] Fix reversed search URLs - * [9e66c9] Dont prepend URLs if haystack is not enabled - * [fbc2f3] Handle for the filter_set being empty - - [ Simone Dalmasso ] - * [f81491] move the absolute creation to the layers post save - - [ Jeffrey Johnson ] - * [cb7cc1] Uncomment haystack in setup.py - * [728bcb] Comment haystack settings - * [9952fd] Only import haystack in resourcebase_api if enabled - - [ nathanhilbert ] - * [90db1b] Fix all Windows binary file opens - - [ Jeffrey Johnson ] - * [4d197c] Fix up permissions in geonode/maps/views.py - * [d2d20f] Fixup perms per new guardian stuff - * [7f124d] Re-enable geoserver by default - * [b5f30d] Fix references to old profile setup in map metadata: - * [aef57b] Move encode/decode functions to utils and remove extraneous stuff? - - [ nathanhilbert ] - * [9151a6] register geoserver signals in Layer tests - - [ DHohls ] - * [171ddc] Added django-admin-bootstrapped as dependency - - [ Simone Dalmasso ] - * [ffdfe5] move the absolute url logic to a resource base signal - * [f65574] fix document form and view - - [ nathanhilbert ] - * [7a2304] paver start and paver stop now work for windows - * [14e5df] added new win_install_deps function in windows to be referenced in windows install documentation - * [db0939] update to quick install for windows - * [3289c8] Created win_devinstall doc - * [4f70f1] Added reference to _install_win_devmode - - [ Simone Dalmasso ] - * [0e6399] comment admin-bootstrapped as is clashing with model translation and assets.min.js in the admin - * [468ef2] fixed autocomplete issue in layer admin - * [e55aa2] fix thumbnail management and make it working also in debug mode - * [c808a7] fix ajax lookup for users and groups fixes #1568, thanks @garnertb - - [ Tyler Garner ] - * [fe67fb] Add missing
tag. - - [ Matt Bertrand ] - * [aecaf6] Use haystack to perform filtering; fix links in search/explore item list; fix vector/raster counts; use haystack if enabled on explore pages - * [dcf4a5] New settings value for filtering by permissions in haystack - - [ Simone Dalmasso ] - * [467542] merge mbertrand pr fixes #1569 - * [87586a] revert to absolute url in resource base snippet - * [a05be6] change absolute_url to detail_url in resource base models for consistency with groups and ppl - * [d59801] add thumbnail url to haystack search - * [14287d] default sort by date - * [cb5204] fix thumbnail url in activity list fixes #1526 - * [2dffe0] recompile the css to get back missing styles - * [aca2bb] highlight selected sort filter - * [3e36ea] fixes groups in permissions form and fixes #1552 - * [80be26] fix group management in permissions form - * [a6951d] fix profile items - * [1a724c] small fixes to the make release docs - * [7d239f] add the average rating to resource base - - [ Matt Bertrand ] - * [a3994e] Added optional setting to update facet counts based on filtering choices when using Haystack (HAYSTACK_FACET_COUNTS) Renamed optional setting to turn off security permission pre-filtering (SKIP_PERMS_FILTER) Simplified/fixed Haystack bbox filtering Fixed haystack indexing of keyword values - - [ Simone Dalmasso ] - * [d13a54] remove the user api as unneeded and some fixes - * [afb6d0] some api docs and small security update - * [161818] update the permissions widget screenshot and some api doc - - [ Ariel Nunez ] - * [1768d7] Fixes issue found in Ubuntu 14.04 with request.body - - [ Matt Bertrand ] - * [7ad65b] Add a text filter to search/explore pages - indicate what text was searched for if any, and allow user to modify this filter. Based on main search input form. - - [ Tyler Garner ] - * [6d5af7] Add a GeoGit contrib package. - * [ab0e29] Add a flake8 configuration to setup.cfg. - * [033fdd] PEP8 fixes in the api app. - * [b57cca] PEP8 fixes in the base app. Fixes #1577. - * [de74d6] PEP8 fixes for the catalogue app. Fixes #1578. - - [ capooti ] - * [16ec9c] Bump geonode-user-accounts 1.0.2 - - [ Tyler Garner ] - * [e3e8cb] Fix unresolved description variable in the base models. - - [ Matt Bertrand ] - * [96db67] Fixed PEP8 violations - - [ Simone Dalmasso ] - * [eafb5c] fix pep8 violations in api - * [2557bb] correctly assign permissions when anyone is checked fixes #1595 - * [fab248] add correct admin for the Profile management fixes #1594 - * [0eb7f9] fix pep8 violations in people app fixes #1585 - * [0531ca] fix layers pep8 violations fixes #1583 - * [68eb4d] use the public location for ows url - * [61dbe9] check if the q param is passed in js - - [ Brylie Oxley ] - * [978387] Fixed broken pyscw tools link - - [ Tyler Garner ] - * [22d0ff] Use the PickleSerializer so UploadSessions can be properly serialized. Fixes #1601. - * [f1c919] Remove get_or_create logic when creating layer POCs since all users have profiles now. - * [e9f715] PEP8 fixes in the contrib app. Fixes #1579. - * [67066c] PEP8 fixes in the social app. Fixes #1589. - * [02d70a] PEP8 fixes in the proxy app. Fixes #1586. - - [ Simone Dalmasso ] - * [e0b812] fix pep8 violation in security fixes #1587 - * [3ff726] fix pep8 violations fr upload fixes #1590 - * [b73ff9] fix pep8 documents violations fixes #1580 - * [12dde9] fix map pep8 violations fixes #1584 - * [76107d] fix groups pep8 violation fixes #1582 - * [c3e225] fix geoserver pep8 violations fixes #1581 - * [477f6b] fix geonode pep8 violations fixes #1576 - - [ unknown ] - * [b388e1] Changed paver win binaries to consistent sources - - [ capooti ] - * [ffd222] Bump geonode-user-accounts 1.0.3. Fixes part of #1600. - - [ Nathan Hilbert ] - * [776269] Updated Win install instructions - - [ Simone Dalmasso ] - * [9ade82] fix the failing smoke test - - [ Tyler Garner ] - * [f0aa8f] Bump geonode-announcements to 1.0.3. Fixes #1600. - * [c55c21] Fix the last PEP8 warning and add flake8 test to travis. - - [ Simone Dalmasso ] - * [9522dd] clean change log from missing final7 and update pavement to work on ubuntu 14.04 - - [ Tyler Garner ] - * [16e083] Load GeoNode CSS assets after vendor CSS when DEBUG_STATIC is true. - * [fe679d] Move activity.less to base.less and add activity styling. Fixes #1572. - * [5b34d0] Date picker improvements. - * [44bb72] Minor style fixes. - * [9c385b] Add ZeroClipboard.min.js and moment.min.js to the gitignore. - - [ Simone Dalmasso ] - * [c3da15] Revert "clean change log from missing final7 and update pavement to work on ubuntu 14.04" - * [9e7dab] symlink the .git dir to make git-dch to work - - -- Simone Dalmasso Mon, 21 Jul 2014 07:39:02 +0000 - -geonode (2.0.0+thefinal7) precise; urgency=high - - * UNRELEASED - - -- Ariel Nunez Fri, 04 Apr 2014 10:52:20 +0000 - -geonode (2.0.0+thefinal6) precise; urgency=high - - [ capooti ] - * [b5be94] Added a geonode_type property in resourcebase and refactored some code. - * [5e8a67] Removed a reference from doc to the ACCOUNT_ACTIVATION_DAYS setting, as we use django-user-acconts now an not django-registration anymore. - * [6bacae] This fixes #1348. - * [a2749c] Fixed #1339 for maps. - * [f07d54] Fixed #1339 for documents. - * [7de68a] Setting the sort order for profiles. - - [ mikefedak ] - * [878f7d] Update geonode.updateip - - [ capooti ] - * [66b4fb] Now the search text box works as expected. Fixes #1334. - * [8917f3] Add configurations to let the GeoNode admin to select which data and metadata formats are available for donwload. - * [fb7b45] Importing the settings from correct location and removing an unused import. - - [ Tom Kralidis ] - * [a43952] s/geopython.github.com/geopython.github.io/ - * [261ead] s/geopython.github.com/geopython.github.io/ - - [ Tyler Garner ] - * [75f7b8] Default a new GeoGit repository's branch to master. - - [ capooti ] - * [bc358a] Fixed some broken link in help page. - - [ Tom Kralidis ] - * [4f29c6] Update setup.py - - [ capooti ] - * [a1b248] Fixed the DOWNLOAD_FORMATS_RASTER setting. - * [13826c] Now it is possible to add multiple keywords when using importlayers. Fixes #1361. - * [2b8817] This fixes #1360. - - [ Tom Kralidis ] - * [c28333] safeguard if value is None (#1260) - * [68ce38] fix CSW SpatialRepresentationType output given model changes (#1362) - * [4593e8] Update full_metadata.xml - - [ Brylie Oxley ] - * [1212f1] Updated docs to include Geoserver default credentials. - - [ mwengren ] - * [eb8df8] Change advertised property test in gs_slurp to account for various default values in different gsconfig versions - - [ Simone Dalmasso ] - * [e8582c] fix the padding in search pages and make collapsed the spatial filter - - [ Vivien Deparday ] - * [d7e211] Add comment for DEBUG_STATIC to avoid missing dependencies error - - [ Benjamin Adams ] - * [a37ccd] Altered WCS link generation method width/height - - [ Michael Diener ] - * [ab5784] added some web links - - [ Tyler Garner ] - * [11424f] Ensure proper configuration when using the geonode.importer backend. - - [ Simone Dalmasso ] - * [b8a368] Add comments on DEBUG and DEBUG_STATIC in the local_settings.py - - [ capooti ] - * [28753b] Updatelayers -w option nows correctly skip any layer if the workspace does not exist. - * [13cc84] This fixes #1378. We should refactor the whole thing, but for now it works. - * [7d8131] Removed a small typo. - * [5ad645] Enable resource link selection in document metadata form. - * [4458c8] Added South Sudan to base COUNTRIES enumerator. - * [f3d822] Now using GeoExplorer 4.0.2 and added back the MousePosition control. Thanks Ariel for packaging! - * [d1b504] Manage the previously unhandled case of a document with unlinked contents. - - [ Daniel Kastl ] - * [abcafa] 'Japanese' was wrongly translated as 'person' instead of 'language' - - [ Tom Kralidis ] - * [e950e7] update pycsw - * [5a1e86] bump pycsw - - [ Tyler Garner ] - * [7edea2] Generate thumbnails for documents. - - [ Simone Dalmasso ] - * [24ef97] make search extent map show up when not url params are present - * [5ee5ea] include spatial search in every list page but the home - * [8071ae] don't include the ext css in the spatial search - - [ Vikas ] - * [92d52d] Add Tweet button Under share this layer - * [7b1398] Created a new template social_links.html and incuded it in layer_detail, map_detail and document_detail - * [7e1490] Added a template at /avatar/add/ which gives correct message when a avatar of size >1Mb is uploaded - * [82fc69] Added a check whether user has uploaded a file or not before clicking on uploading and also improved the errors page to redirect them to the error div in documents_upload.html - * [b5f691] Fixed download link at document_list.html so that now it points to the correct download link - * [e9850b] Removed tweet button which used javascript, added new tweet button which does not use javascript - - [ capooti ] - * [46a63c] Removing the import to geomodels search (using geodjango) for now, as it is breaking search. - * [2b517e] Refuse from previous commit. - * [26f0c3] Reverting last commits, as it is breaking things (sorry!). - * [1981e5] Changing map position coordinates in map composer to 4326. - * [6d08d0] Fixes #1338. Drawback: requires a small schema migration (included). - - [ Simone Dalmasso ] - * [af2ef4] quick fix to avoid xss in the search widget - * [b76e4a] clean search term in other select2 widgets - - [ Matt Bertrand ] - * [71562f] Security fix for map composer title,abstract - - [ Tom Kralidis ] - * [720ecc] bump pycsw - - [ npmiller ] - * [2b9e9e] Fix inifinite redirection when trying to remove something without perms. - - [ state-hiu ] - * [fd114a] changed show/hide to more info in response to issue 1399 - - [ capooti ] - * [6f3302] Removed the DOCUMENTS_APP setting. If documents is installed it will be used by geonode, no neet to set any setting. - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 04 Apr 2014 07:10:55 +0000 - -geonode (2.0.0+thefinal5) precise; urgency=high - - * UNRELEASED - - -- Ariel Nunez Tue, 21 Jan 2014 20:08:25 +0000 - -geonode (2.0.0+thefinal4) precise; urgency=high - - [ Matt Bertrand ] - * [6291dc] Update ows_url of local MapLayers to match new IP - - [ Ariel Nunez ] - - -- Ariel Nunez Tue, 21 Jan 2014 19:27:56 +0000 - -geonode (2.0.0+thefinal3) precise; urgency=high - - * [2cd92c] Updated base.css with latest changes to base.less - - -- Ariel Nunez Tue, 21 Jan 2014 19:00:24 +0000 - -geonode (2.0.0+thefinal2) precise; urgency=high - - * UNRELEASED - - -- Ariel Nunez Tue, 21 Jan 2014 18:40:42 +0000 - -geonode (2.0.0+thefinal1) precise; urgency=high - - [ Ariel Nunez ] - * [fc2701] Added Biboy to AUTHORS. - - [ Simone Dalmasso ] - * [532f73] small update on the migrations doc - - [ capooti ] - * [2b9150] Updated migrations for running from an effective 1.2 instance (WFP one was slightly updated). - * [a7bb5e] Fixed the failing case of migrations with an empty layers table. - * [d2d21e] Added the host in psycopg2 connection string as without it causes an error in installation procedure, even if default should be localhost. - - [ Simone Dalmasso ] - * [d6965e] put back the correct content wrapper padding - * [4e37e4] Add something on how to add spatial columns to the db - * [b47d4c] add the geonode-updateip after migrations - - [ Ariel Nunez ] - - -- Ariel Nunez Tue, 21 Jan 2014 05:47:09 +0000 - -geonode (2.0.0+thefinal0) precise; urgency=high - - [ Matt Bertrand ] - * [bdea4a] Add external services to user help doc - - [ menegon ] - * [755521] Enable proxy for https connections - - [ Mike Fedak ] - * [b00b19] \Adds static legend to layer detail - - [ Ariel Núñez ] - * [b7e24a] Adding new AUTHORS - - [ Vivien Deparday ] - * [d07643] Update AUTHORS - - [ Bebsher L. Salvio ] - * [14fd20] Fix style of pagination pager. - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 17 Jan 2014 19:31:08 +0000 - -geonode (2.0.0+rc13) precise; urgency=high - - [ Elliot Bradbury ] - * [5a6835] FGDC Topic Category extraction - * [062b7e] Assigns TopicCategory to Layer using extracted metadata. - * [c7b32c] Added category_list to advanced search context for category filter - * [d1cca9] Added category filter - * [3807cd] Added support for capturing multiple keywords from FGDC metadata. - - [ nathanhilbert ] - * [425fb7] added OL to search - * [2db0ba] added UI for search by extent - * [54e8a3] UI cleanup for extent search - * [e70db9] added extent filter to search results page - * [c28c69] removed extra openlayers in favor of OL wrapped in ExtGeo - * [07f06c] added geoexplorer JS packages in search templates and wrapped in Ext - - [ capooti ] - * [7ec602] Some small update to the migration documentation. - * [0be61b] Added a migration for adding the new base.license model. - - [ Tyler Garner ] - * [8f9631] Add the SOCIAL_BUTTONS setting to context_processors. - - [ Ariel Núñez ] - * [4cad1f] Added Nathan Hilbert to AUTHORS - - [ Tyler Garner ] - * [a36f8f] Fix spelling error and remove duplicate entry in AUTHORS. - - [ Ariel Núñez ] - * [876976] Added Elliot Bradbury to AUTHORS - - [ capooti ] - * [27c8a5] Fixed the migration's documentation. - * [45a141] Now title for documents can use lowercase chars. Something from css was conflicting with the field id. - - [ Ariel Nunez ] - * [722b87] Added pyproj instructions when using pip 1.5 - * [a7653f] Added pyproj exception to paver setup - - [ mikefedak ] - * [d79eda] Add details to fix pyproj install on Pip 1.5 - - [ vdeparday ] - * [ba34da] It is not required to create a new folder and it is actually not used after - - [ state-hiu ] - * [fb7133] Added missing inbox link from admin dropdown in response to issue 1267 - - [ warex03 ] - * [24bd64] Fixed viewby padding and Search menu's content-wrap padding in base.css - * [627890] Modified the base.less file - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 17 Jan 2014 01:28:16 +0000 - -geonode (2.0.0+rc12) precise; urgency=high - - [ Ariel Núñez ] - * [438610] Update AUTHORS - * [826578] Added cspanring to AUTHORS - - [ Ariel Nunez ] - - -- Ariel Nunez Tue, 14 Jan 2014 05:50:17 +0000 - -geonode (2.0.0+rc10) precise; urgency=high - - [ Micah Wengren ] - * [b02aef] Changed Django get params used in wms_links lookup to move url to defaults dict. Prevents duplicate WMS links via repeated updatelayers calls - - [ capooti ] - * [b50b52] Refactoring migrations. - * [4fa053] Added the migration for adding the resourcebase_ptr_id field in layer. - * [ecdf77] Added the migrations to move layer stuff to base. - * [6330d2] Some more steps added. - - [ Micah Wengren ] - * [8f21ba] Decode name value into localized string before get_or_create query. Now prevents duplicate WMS download links due to changed bounding box in url - - [ capooti ] - * [56d44a] Intermediate commit for Simone. - * [ac1088] Now resource_base is correctly populated with layers. - * [4bdc09] Now importing spatial representations, restriction codes, topic categories and regions from layers to resourcebase. - - [ Jeffrey Johnson ] - * [de9ce4] Update dns entry on jenkins-geonode-aws.sh - * [ac623b] Use existing venv for aws task - * [c4cd4a] Remove trailing slash in jenknis-geonode-aws.sh - - [ Matt Bertrand ] - * [554fc9] Rename files inside zip to match new layer name - - [ Ariel Núñez ] - * [aa650e] Added license to info panel - - [ Calvin Metcalf ] - * [fa6080] use wgs84 for geojson - - [ Ariel Núñez ] - * [f5f69e] Added Adam Ziaja to Contributors - - [ Tom Kralidis ] - * [882505] add license back to ResourceBase (#1310) - * [928848] fix constraints XML - - [ capooti ] - * [2879a9] First working version for 1.2 to 2.0 migrations. - * [4415c8] Now the permissions, styles and other notable stuff is migrated. - - [ Simone Dalmasso ] - * [8050c7] don’t use the geonode settings - * [2879cc] fix the missing one - - [ capooti ] - * [e8da25] Last commit before doint the migration PR. - * [c2b1e2] Added documentation for migrating from 1.2 to 2.0. - - [ Ariel Nunez ] - - -- Ariel Nunez Tue, 14 Jan 2014 05:09:52 +0000 - -geonode (2.0.0+rc8) precise; urgency=high - - [ Simone Dalmasso ] - * [8ef921] add missing double quotes in layer detail - * [b967ec] add missing slash in proxy url - * [b45f00] fix the clear options button in advanced search - * [4a9931] keep open the type filters - - [ capooti ] - * [d0583a] This fixes #1311, thanks @bartvde! - - [ Tyler Garner ] - * [ee93a7] Fix typo in the default PROXY_URL setting. - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 20 Dec 2013 13:57:55 +0000 - -geonode (2.0.0+rc7) precise; urgency=high - - [ Tyler Garner ] - * [4ba656] Document the SOCIAL_BUTTONS setting. - - [ capooti ] - * [8344d8] This fixes #1301. - - [ Tyler Garner ] - * [9015d8] Fix ALLOWED_DOCUMENT_TYPES typo in the documentation. - - [ capooti ] - * [dabffc] Added some more popular file extensions to ALLOWED_DOCUMENT_TYPES. - - [ Simone Dalmasso ] - * [6242a8] add missing import in helpers for remove-deleted option - - [ Reinier Battenberg ] - * [b2ab31] doUpload now returns false so the page does not scroll upwards when Upload is clicked - - [ Calvin Metcalf ] - * [4c05f7] correct command - - [ Tyler Garner ] - * [209717] Fix the quick start url to point to the users tutorial in the docs. - - [ capooti ] - * [ba3d72] Layer type (vector/raster) on layer list page is back. - - [ Simone Dalmasso ] - * [a5f4ec] use default type if not specified - - [ capooti ] - * [8063d6] Reset the default_type to layer. - * [1257b7] A couple of selection filters were not collapsed by default. - - [ Simone Dalmasso ] - * [7afb0b] don’t run the integrations tests in travis. - * [87ae0b] trigger a new build - * [46d58f] don’t grab geoserver in travis - * [5701db] just comment out the before script - * [c73319] remove the old docs - * [b5f404] remove the outdated customize doc page - - [ Jeffrey Johnson ] - * [eb211c] Working on jenkins-geonode-master.sh during migration to new build server - * [e1ad5a] More work on migrating build server (pointing to old server for downloads for now) - * [f4c37c] Update java home for oracle jdk in jenkins-geonode-master.sh - * [60cacc] Skip tests when building geoserver extension (temporary) - * [a2942d] Disable deb building for now in build_geonode-geoserver-ext-deb.sh - * [1981c5] Change back to normal build.geonode.org url in pavement.py - * [0f3468] Dont try to copy the deb that we are not currently building. - * [87fddb] Comment out one problematic rm for now - * [cd7744] Reenable sourcing of ~/.bashrc now that the ec2 tools are configured - * [ff8e90] Re-enable integration tests on jenkins server - - [ Tyler Garner ] - * [26c157] Improve security for the proxy view. Fixes #1308. - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 20 Dec 2013 03:29:25 +0000 - -geonode (2.0.0+rc6) precise; urgency=high - - [ Micah Wengren ] - * [9f46d4] Add --remove-deleted option to updatelayers. Remove layers from GeoNode that no longer exist in GeoServer (incl --skip-unadvertised filter). Log results in stdout and debug logs - * [cd324c] Modified logic for GeoServer layer comparison for marking valid GeoNode layers - * [3d56a5] Streamline debug logging statements for GeoNode resource comparison with GeoServer, cleanup - * [061646] One more debug logging line for better reporting in matching iteration - * [1d41ae] Added code to remove ratings and comments for a layer directly in gs_slurp. Avoid calling geoserver_pre_delete - - [ Tyler Garner ] - * [19a9dd] Remove "or groups" from the upload template until group permissions are supported. Fixes #1262. - - [ Micah Wengren ] - * [081cc3] Removed cleardeadlayers command and updated documentation with descriptions for all updatelayers options - - [ Ariel Nunez ] - * [6e6b03] Updated messages - * [087339] Fixed translation problem in upload templates - - [ Simone Dalmasso ] - * [b1dcfc] docs, add the port 8000 to paver start mode - * [8fe84d] add django debug toolbar - - [ Micah Wengren ] - * [5be24f] Delete layer tag (keywords) references when layer is deleted - - [ Jeffrey Johnson ] - * [9ea397] Update domain name to demo.geonode.org in fabfile.py - - [ Tyler Garner ] - * [44eed6] Remove the cleardeadlayers reference from the Django-apps page. - * [e6049c] Improve the settings documentation. - * [09fde6] Document the GeoNode paver commands (Fixes #1169). - - [ Simone Dalmasso ] - * [dbd328] fix typos - * [9d925c] small additions to the geonode_project usage - * [4b3428] show complete extension for invalid files - * [5e6efa] fix a not closed div - * [6d96d4] add few lines on the issue tracker - * [e0ebca] remove the devprocess reference - * [5c571c] Add something on development tools - * [674b21] add setup on vagrant guide - - [ Calvin Metcalf ] - * [b90172] #1276 replace pil with pillow - - [ Tyler Garner ] - * [b6495c] Add the document model to the actstream settings. - - [ Simone Dalmasso ] - * [aefcfb] fix rating in detail pages for unauthenticated - * [46bb06] add missing port in vagrant doc - * [2af05b] add __init__.py file in the base commands folder - - [ Paolo Pasquali ] - * [8602b3] Fix #1266 - * [c749fb] Some Upload Layers style - * [b71696] More styles for Upload Layers template - * [1c8a4c] Fix Layer Replace template - * [761c64] Add icon placeholder in documents search list #1269 - - [ Simone Dalmasso ] - * [4d7f28] don’t delete the resource but the store recursively - * [888e92] apply a patch to helpers - - [ Paolo Pasquali ] - * [8d255b] indentation only - * [883ae0] Fix Activity layout - * [d903a9] Some style in Activity Feed page - * [5deb67] Fix Activity Layout - - [ Tom Kralidis ] - * [304427] add timeout configuration to handle long requests (#1279) - - [ Tyler Garner ] - * [6fa8f8] Remove the GeoNode projects page from the documentation and redirect links to geonode.org/gallery/. Fixes 1147. - - [ Paolo Pasquali ] - * [6f7d9d] Add thumbs and style to activity templates - - [ Tyler Garner ] - * [01ab69] Update javascript in GeoNode page to reflect current static setup. Fixes #1175. - * [1d431e] Fix TIMEOUT typo in the settings documentation and specify the time units as seconds. - - [ Simone Dalmasso ] - * [66686c] Add just few pdb commands. - * [1689a5] add missing __init__ file - - [ Paolo Pasquali ] - * [6e9681] Add thumbs in profile resources - - [ Calvin Metcalf ] - * [374b05] make social optional - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 04 Dec 2013 16:27:16 +0000 - -geonode (2.0.0+rc5) precise; urgency=high - - [ Ian Schneider ] - * [9a94f3] partial fix for #1257, map download null.zip - - [ Ariel Nunez ] - * [051088] Change priority of geoserver's package - - -- Ariel Nunez Wed, 13 Nov 2013 20:00:37 +0000 - -geonode (2.0.0+rc4) precise; urgency=high - - [ Tom Kralidis ] - * [810cee] strip csw:GetRecordByIdResponse element if it exists - * [71900a] no error message anymore - - [ Tyler Garner ] - * [22eaac] Bumped the django-geoexplorer version to 3.0.5, fixes #1036. - - [ Ariel Nunez ] - * [966e1c] Fixed 500.html - * [57735d] Removed block in 500 template - * [dce0a9] Fixed #1256 - * [be37d4] Stopped downloading a data.zip. - * [eacd42] Do not rely on data.zip, use geoserver's - * [8e220a] Raise exception if map download fails, do not fail silently. - - -- Ariel Nunez Wed, 13 Nov 2013 13:44:04 +0000 - -geonode (2.0.0+rc3) precise; urgency=high - - [ Ariel Nunez ] - * [39640c] Pegging the setup.py file to Django 1.5.4. - * [f4bff4] Django 1.5.5 is already out - - [ Simone Dalmasso ] - * [49d125] use public url in the map download page - * [326059] use an initial query in search - - [ Ariel Nunez ] - * [399211] Added fixsitename command, refs #498 - * [630566] Added fixsitename call to updateip, fixes #498 - * [560df6] Fixed missing thumb behavior, fixes #998 - - [ Bart van den Eijnden ] - * [62ac08] when login is successfull, update the CSRFToken in OpenLayers.Request.DEFAULT_CONFIG.headers - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 08 Nov 2013 18:32:28 +0000 - -geonode (2.0.0+rc2) precise; urgency=high - - [ Simone Dalmasso ] - * [fd4f94] use internal location where possible - * [1cb336] fix thumbnail and links - - [ Michael Diener ] - * [012050] update to reflect main github readme dev installation directions - - [ capooti ] - * [9dddf0] Now in the categories list in layers and maps page the category with is_choice set to false are not displayed. - * [00fc80] Added the MODIFY_TOPICCATEGORY to allow topic categories metadata customization when strictly necessary. - - [ mdiener21 ] - * [79efbb] fixes to reflect 1 to 1 the readme.rst installation method - - [ Barbara ] - * [ab68aa] add image to translation doc - * [7370fa] add other options to translation docs - - [ Tyler Garner ] - * [d169a6] Added the block.super variable to templates that use the extra_head block to fix #1241. - * [e7a7da] Fixes javascript and styling problems in the replace layer view. - * [12e8de] Fixes #1242. - - [ capooti ] - * [10e654] Added the documentation for the MODIFY_TOPICCATEGORY setting, and reorganized some of the settings in a "Metadata Settings" section. - - [ Barbara ] - * [8ca66b] add instructions on geonode project - * [905a60] modified setup - * [a20ccf] old setup - * [b1a74a] added toctree - - [ b-angerer ] - * [faf723] modify translation docs - * [74a83f] added translation - * [4c9f47] modified - * [cdc9da] modified - * [41aa56] modified po files - no translation added - * [0a5dab] modified po files - no translation added - * [38e0ab] modified po files - no translation added - - [ Ariel Nunez ] - * [2fd7c1] Added on_delete=models.SET_NULL to thumbnails to prevent them from deleting layers, thanks Fuyou on #django - - [ capooti ] - * [661fed] Now categories, keywords and date filters in layers, maps and docs page are collapsed by default. Thanks @ppasq! - - [ Simone Dalmasso ] - * [60e07f] use the dot in the cdv and kml extensions in uploader - - [ Tyler Garner ] - * [560bc9] Added support for Basic authentication end-to-end headers to the GeoNode proxy view. - - [ Micah Wengren ] - * [84fcbe] Added parent template references for block extra_head in a couple templates where missing - - [ Tyler Garner ] - * [9c49bd] Small js and html improvements to the incomplete uploads section of the upload page. - - [ b-angerer ] - * [b73d28] added german translation - - [ mdiener21 ] - * [f4ace9] update title text - * [523b12] small text fix - - [ Tyler Garner ] - * [38ccdd] Fixed a JS error and made strings translatable in the actor template. - * [e68331] Replaces the JQuery UI progressbar with the Bootstrap progressbar and safely checks event targets. - * [19ab7b] CSS and JS cleanup in in the map_download template. - * [47088a] Set up all template strings for translation. Fixes #617. - - [ Simone Dalmasso ] - * [493146] fix the maps thumbnails internal url - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 06 Nov 2013 13:48:28 +0000 - -geonode (2.0.0+rc1) precise; urgency=high - - [ Tyler Garner ] - * [7aa29e] Fixes #1229. - - [ Michael Diener ] - * [475150] removed redundancy and added some more explanations - * [db35e3] added step 1,2,3 text and alignment - - [ crabtree ] - * [9403f1] using django url templatetag in map_remove.html instead of raw url path - - [ Michael Diener ] - * [0ff51c] added downstream projects on github to the list - * [fbb128] fix width of table - * [ad2d51] removed librelist email for geonode-users@googlegroups.com - - [ vdeparday ] - * [947ea5] Make sure the endpoint /gs/updatelayers pass a username - - [ Simone Dalmasso ] - * [bdf927] use libxslt1-dev in the dependencies - * [9dea3c] update also the readme - - [ mdiener21 ] - * [d0a16e] added doc how to translate documentation - * [595b8b] added doc how to translate documentation - * [8d8be3] removed tabs to spaces - - [ Ian Schneider ] - * [b89797] ensure mvn install target called for zip plugin - - [ Ariel Nunez ] - - -- Ariel Nunez Mon, 21 Oct 2013 13:24:29 +0000 - -geonode (2.0.0+beta64) precise; urgency=high - - [ Tyler Garner ] - * [62408c] Added OGC_SERVER settings documentation. - * [5fda6a] Added more apps to the documentation. - * [af1e74] Fixes spelling errors/docstrings. - * [fa1a02] Added more django apps and settings to the documentation. - * [f7d0c7] Fixed spelling errors and added the geonode.security.middleware documentation. - * [e0c3ea] More docs cleanup. - - [ mdiener21 ] - * [9a70cf] added link to install Geonode for developers to Admin install tutorial - * [231f6d] documentation install spelling fixes and streamlined workflow installation - * [8dac6a] renamed manual installation to configure installation to reflect content, this was never a manual installation document - * [15c0ec] fix toctree - * [d216d0] small fixes - * [a53fda] updated to recommend seeing postgis.net homepage for installation - * [85c707] reorginize location - * [dd84d9] renamed to reflect configuration documentation after geonode install - * [5b22d9] fix toctree added dependencies configure_installation - * [224319] updated links and structure - * [6d8c4c] merged manual and complete install into one, still needs review - * [798c5d] renamed manual install to configure after install to reflect doc content - * [90d7ec] updated links to reflect new docs - * [4743e0] updated links and title for clarity - * [2dafa5] removed no longer needed files, moved img dir - - [ Michael Diener ] - * [0ca464] Update settings.py - - [ Ariel Nunez ] - * [b343e8] Upgrade gsconfig to 0.6.7 - - [ mdiener21 ] - * [a14f60] added level 5 header style to doc ref - * [c31378] updated title style to conform to doc style guide - * [8df973] fix style error - * [00362e] removed duplicate old used dependancy django-user-accounts==1.0b7 - * [1ecb08] fixed structure remove duplicate text..still needs work - - [ Tyler Garner ] - * [9f41cc] Fixed the regions attribute to only display when an object has regions in detail views. - * [640b21] Fixed the announcements. - * [67b952] Initial settings work. - - [ mdiener21 ] - * [191c45] intro text updated to be more specific - * [ed7c9c] cleared up some text and made it easier to read..ongoing work in progress - * [f63b76] fix title tag - * [816603] added link to configuration page to manual install - * [b0b3d4] added ordered lists to instructions and cleaned up redundant text - * [928aad] fixed ordered list to show proper numbering - * [d2c5b0] moved reference links to index page and removed reference.txt - * [1472a7] moved dependencies into complete install - * [1b61c0] moved dependencies into complete install - - [ Guido Stein ] - * [fbc5db] update dev install ubuntu in readme - - [ mdiener21 ] - * [b0acb0] update developer install to reflect README updates - - [ Simone Dalmasso ] - * [48253f] add dialogs template - - [ Tyler Garner ] - * [9b6868] Don't try to delete default styles if they don't exist. - - [ Christian Spanring ] - * [cead59] shows and saves default style on layer manage style page - - [ Tyler Garner ] - * [c2aac3] Finished the settings documentation. - * [c9235a] Spelling/docstring fixes. - * [35cec7] Cleaned up spelling errors and capitalization inconsistencies in the docs. - * [361246] Cleaned up spelling errors and capitalization inconsistencies in the docs, added the ROGUE project to the downstream GeoNode projects. - - [ mdiener21 ] - * [c5cc87] fix title underline too short - * [89bcd2] fix indent to short - * [fad018] added settings to doc tree - * [f646db] added settings ref to index - - [ Simone Dalmasso ] - * [e0a7db] update the local settings - * [27ba1c] enable WPS by default and let updatelayers to update statistics - * [efcb92] use ogc server public url in ows links management - - [ Tyler Garner ] - * [8ec335] Removes the layer styles tool from the GeoExt widget in the map detail page. Fixes #1126. - - [ mdiener21 ] - * [a5e542] reduce title redundancy - * [41babe] fix title rst style reduce title redundancy - * [09a6ed] fix title rst style - - [ Barbara ] - * [ec281b] updated manual installation - * [687ae8] updated index - * [ebce68] more changes - * [80df8e] update - - [ Tyler Garner ] - * [432d48] Activity stream enhancements. - - [ Barbara ] - * [84b4f4] updated - * [ed11b5] updated index.txt - * [15d561] updated configuration - * [929e96] updated postgis - * [c232d5] update - * [1afffc] updated - - [ mdiener21 ] - * [183ffe] complete install re-worked - - [ Barbara ] - * [1c9fee] change link to configuration - * [e1e00a] small fixes on shape - * [b1612a] small fixes - - [ Tyler Garner ] - * [ab6fc3] Small JS additions to allow the importer backend to finish CSV uploads. Fixes #1224. - - [ Barbara ] - * [f35b19] did some todos - - [ Michael Diener ] - * [c5ef8c] manual config using ubuntu config file updated for clarity - * [537905] removed un-neccessary text - - [ Tyler Garner ] - * [be906b] Uses the OSMSource plugin for the OSM layer in the default basemap. Fixes #142. - - [ Barbara ] - * [5a6841] file deleted - - [ Ariel Nunez ] - - -- Ariel Nunez Thu, 10 Oct 2013 18:40:04 +0000 - -geonode (2.0.0+beta63) precise; urgency=high - - [ Tyler Garner ] - * [3e55e0] Adds the GEOGIT_DATASTORE_DIR settings variable to specify a default location for geogit stores. - * [7707c2] Remove PUBLIC_LOCATION from the OGC_SERVER settings test case. - - [ Ariel Nunez ] - * [d36d16] GEOSERVER_BASE_URL should point to the public_url - - [ Simone Dalmasso ] - * [70192f] put back the PUBLIC_LOCATION to 8080 for dev mode - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 25 Sep 2013 14:48:33 +0000 - -geonode (2.0.0+beta62) precise; urgency=high - - [ Tom Kralidis ] - * [c2e4ec] add xsd:double (#1128) - - [ Tyler Garner ] - * [7c4fc1] Added missing string formatter. - - [ Ariel Nunez ] - * [fd3ff1] Fixed location of geoserver in settings.py - * [935125] Fixed geoserver url retrieving in local settings - * [59e100] Do not set a default for PUBLIC_LOCATION, instead the code should default to LOCATION if PUBLIC_LOCATION is not set - - -- Ariel Nunez Tue, 24 Sep 2013 15:43:04 +0000 - -geonode (2.0.0+beta61) precise; urgency=high - - [ mwengren ] - * [d1c9ca] Added an option to updatelayers to check for 'advertised' status of GeoServer layers and skip if false. Requires modified gsconfig.py - * [65d800] Change to --skip-unadvertised filter to accept 'None' values: in some cases there will be no 'advertised' property in REST API representations - * [30f09e] Change --skip-unadvertised option help msg - - [ Christian Spanring ] - * [a54a7d] adds group layer publishing for local map layers to maps - * [931303] adds grunt watch for CSS development - - [ Micah Wengren ] - * [c4f7a9] Change geoserver_post_save to use correct url reference - * [3caae7] Bump gsconfig version requirement to 0.6.4 - - [ Tyler Garner ] - * [8497ef] Fixes issue 951. - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 20 Sep 2013 19:01:38 +0000 - -geonode (2.0.0+beta60) precise; urgency=high - - [ Ariel Nunez ] - * [ab44b2] Added link to ticket in django-admin workaround - - [ Simone Dalmasso ] - * [9f8ed9] make geonode.binary use the available django-admin - * [1f89b6] use the geonode binary in the install script - - [ vdeparday ] - * [8bbfb6] Moving most of the integration section to the relevant user and admin sections; fixes #1163 - - [ Ariel Nunez ] - * [081ff0] Added jetty speedups from #1071, thanks Ian. - * [950e26] Bumped version to 0.6.4 - - [ Michael Weisman ] - * [946bf2] Don't hardcode git branch and ppa - - [ Ariel Núñez ] - * [1c6182] Update AUTHORS - - [ Michael Weisman ] - * [93887b] Check out on geoserver-ext not geonode - - [ Jeffrey Johnson ] - * [5662bd] More work on docs - - [ Ariel Nunez ] - * [0eb192] Remove trailing slash in ALLOWED_HOSTS via updateip, fixes #1184 - - [ Michael Weisman ] - * [23408b] Stop building opengeo suite geondoe ext - * [6a38e8] Build wars - - [ Simone Dalmasso ] - * [4c455d] fix config file install - - [ Ariel Nunez ] - * [876cca] Updated gsconfig to 0.6.6 - - -- Ariel Nunez Fri, 20 Sep 2013 17:34:34 +0000 - -geonode (2.0.0+beta59) precise; urgency=high - - [ Vivien Deparday ] - * [d4f7fb] Update quick_install.txt - - [ capooti ] - * [96c757] Updated the users/layers/upload and users/layers/more doc pages as per #1132. - * [a30562] Removed a screenshot uncorrectly added in last commit. - - [ Ariel Nunez ] - * [602065] Fix django-admin reference in packages, it was breaking debian packages - - -- Ariel Nunez Fri, 13 Sep 2013 14:07:06 +0000 - -geonode (2.0.0+beta58) precise; urgency=high - - [ Tom Kralidis ] - * [5a7ed0] Add blurb about metadata downloads to Layer Information page (#1152) - * [93db6d] Add blurb about layer attributes to layer info docs page (#1154) - - [ Simone Dalmasso ] - * [f6751c] move install demoed under developers workshop - * [a96ecf] adjust a bit the install flow - - [ Tom Kralidis ] - * [5bd5ac] make URL /ows for all, add wcs link - - [ capooti ] - * [7ff5bd] Updated the "sharing layers" screenshots. Refs #1133. - - [ Tom Kralidis ] - * [06ba55] add info on OWS - - [ Simone Dalmasso ] - * [1d6295] Some testing doc - * [ad121c] Add few lines - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 13 Sep 2013 13:18:37 +0000 - -geonode (2.0.0+beta57) precise; urgency=high - - [ mwengren ] - * [59a333] Added support for 'proxy' OGC_SERVER config that allows secondary LOCATION config for public GeoServer access point - * [b8181a] Remove redundant config in settings.py - * [5dd544] Fix syntax error for check of 'CATALOG_WRITE_ENABLED' in layers/models.py - * [2de7c8] Added back in the 'DATASTORE' key in OGC_SERVER OPTIONS dict that was dropped - - [ Christian Spanring ] - * [7518ea] fixes waypoints dependency when DEBUG_STATIC is set. - - [ Jeffrey Johnson ] - * [44a405] Comment out console.log in LayerInfo.js (closes #1087) - - [ Simone Dalmasso ] - * [18237b] add release docs - - [ capooti ] - * [7b868b] Added the gettext_compact option in conf.py file. - * [30415b] Updated the -How to translate GeoNode's Documentation- file. - * [d6100a] Added the initial .po files to git after a tx pull. - * [ce51b8] Updated part of the admin panel section in the admin workshop. Added new screenshots. - - [ Simone Dalmasso ] - * [11156e] some updates on the installation guide - * [cc26e0] move deploy_config under manual installation - - [ Jeffrey Johnson ] - * [eea21d] Move processing to users section - * [9ad8d8] Move data section to users tutorial - * [20dfe4] Moving data section to admin tutorial - * [f3b839] change heading in admin/data/index.txt - * [84633d] Add ssl to tutorials/admin/index - - [ Simone Dalmasso ] - * [481753] fix some installation details - * [80b0fb] use django-admin.py in the installer - * [7f6023] some other small fixes in the installation process - * [e5baa2] fix installation from config file - - [ Paolo Pasquali ] - * [04acda] Fix items layout, home page IE8 bugs - * [8438d2] Restoring scripts - - [ Simone Dalmasso ] - * [8e5d41] add some other admin docs - - [ Ariel Nunez ] - * [8586a7] ows url should link to /ows instead of /wms - - [ Simone Dalmasso ] - * [5a0abb] Take out the geoserver installation from the install script - * [d99367] further work on the admin doc - * [16e906] add the geoserver deployment to the installation with config file - * [24cfc0] fix failing test - - [ Tom Kralidis ] - * [ea78f4] execute WPS statistics only if WPS is enabled on OGC_SERVER (#1128) - - [ mwengren ] - * [bcfc86] Changed ogc_server_settings.ows to use PUBLIC_URL if present - - [ Simone Dalmasso ] - * [541291] update the user registration - - [ capooti ] - * [5fe1e6] Updated first part of the admin data tutorial. - * [ce728d] Updated the admin OSM data tutorial. - - [ mwengren ] - * [615299] Added test cases for PUBLIC_LOCATION, BACKEND_WRITE_ENABLED in OGC_SERVER config - - [ Ariel Nunez ] - * [e4d775] Added service=WMS to get_wms method - - -- Ariel Nunez Thu, 12 Sep 2013 22:42:20 +0000 - -geonode (2.0.0+beta54) precise; urgency=high - - [ Simone Dalmasso ] - * [ce9464] include raty in assets.min.js - - [ Christian Spanring ] - * [e725bc] fixes regex in gruntfile - - [ mdiener21 ] - * [109fe0] merged the files, deleted redundant links, dead links and updated to the new structure - * [0a5318] updated how to transifex localization - - [ vdeparday ] - * [7bd63b] Fixes #1107, the custom Manager was not instantiated and also correct a few type issues in the post_save - - [ Simone Dalmasso ] - * [82d6c4] add admin panel section - - [ mdiener21 ] - * [b1e69e] updated the install procedure - * [20f5c9] finished documentation sphinx guidelines and add logo for example - - [ capooti ] - * [f7b54f] Added a very basic workflow to manage the i18n documentation process. Refs #1099. - - [ mdiener21 ] - * [ae8d99] documentation guidelines updated and completed - - [ Simone Dalmasso ] - * [f9fdc1] reorganize the admin tutorial - * [41792c] more doc install reorganization - - [ Tyler Garner ] - * [b4a1b8] Changed the set_metadata function to safely check for the date key. - - [ Simone Dalmasso ] - - -- Simone Dalmasso Fri, 06 Sep 2013 17:23:24 +0200 - -geonode (2.0.0+beta53) precise; urgency=high - - [ Michael Diener ] - * [b6ea5b] Create quickstart.rst - * [28a3ec] Update quickstart.rst - - [ b-angerer ] - * [23f0fb] new img - * [4dd017] Update tutorial_security.rst - * [84762b] Update permissions.rst - * [826018] Update permissions.rst - * [8a5ac5] Update tutorial_security.rst - * [28528b] Update contribute.rst - * [a9d44f] Create index.rst - * [b5d72d] Create index.rst - * [0a7489] Create index.rst - * [813561] Update permissions.rst - * [93b20a] Create contribute_to_docu.rst - * [9ca409] Create installation_devmode.rst - * [c39d63] Delete install_geonode_in_devmode.md - * [b91cf6] Delete config_geonode_dev.md - * [d5c889] Create security - * [a1b7ed] Create permission_and_security.rst - * [10166a] Create How_to_change_default_db.rst - * [afbf4b] Update How_to_change_default_db.rst - * [6b8490] Update How_to_change_default_db.rst - * [3c01d4] Update How_to_change_default_db.rst - * [07cc86] Update How_to_change_default_db.rst - * [18a18e] Create apache.rst - * [8bc14a] Create tomcat.rst - * [6c9e96] Update How_to_change_default_db.rst - * [193681] Update apache.rst - * [8c2f7f] Update tomcat.rst - * [904373] Update tomcat.rst - * [48bd6a] Update apache.rst - * [788c7e] Update installation_devmode.rst - * [e50002] Update installation_devmode.rst - * [3a5203] Update How_to_change_default_db.rst - * [35f0a9] Create deploy_install.rst - * [28d55b] Update deploy_install.rst - * [0a5433] Create deploy_config.rst - * [ec2832] Update deploy_config.rst - * [9a48b6] Create contribute_to_translation.rst - * [c3d133] Update deploy_install.rst - * [e29fae] Update deploy_install.rst - * [92df47] Update deploy_config.rst - * [4630c8] Update deploy_config.rst - * [0c0d92] Update deploy_config.rst - * [933edb] Update deploy_config.rst - * [25ad12] Update deploy_install.rst - * [4a1675] Update installation_devmode.rst - * [ffc1f5] Update installation_devmode.rst - * [e7acee] Update installation_devmode.rst - * [14c9e6] Update deploy_config.rst - * [51191e] Update installation_devmode.rst - * [209dd6] Update deploy_config.rst - * [225583] Update How_to_change_default_db.rst - * [8d6a4f] Update deploy_config.rst - * [167d81] Update deploy_config.rst - * [aa4ee2] Update deploy_config.rst - * [0dcc86] Update deploy_config.rst - * [f54336] Delete tomcat.rst - * [f151ec] Delete apache.rst - * [2f2bb6] Delete How_to_change_default_db.rst - - [ Michael Diener ] - * [5b95ef] Create index.rst - - [ b-angerer ] - * [edc5d4] Update deploy_config.rst - - [ Michael Diener ] - * [56ddf2] Update index.rst - * [50b08e] copy of original wiki article in geonode wiki moving to main docs - * [a8f1a6] how to contribute to docs from Barbara - * [87ce3b] added link to how to contribute to docs - * [35b931] Update how_to_contribute.rst - * [05627c] Update how_to_contribute.rst - * [55e2e7] Update how_to_contribute.rst - * [ccdce6] Update how_to_contribute.rst - * [3e9f47] Update how_to_contribute.rst - * [0694d2] Update how_to_contribute.rst - * [cebedd] Update how_to_contribute_to_docs.rst - * [d6e2dc] Update how_to_contribute_to_docs.rst - * [0920cc] Create index.rst - - [ root ] - * [ba8c2e] move admin workshops to tutorials - * [f6fa5d] move devel workshop to tutorials - * [5c93ab] move user workshop to tutorials - * [885f1f] modify index - * [db5230] add file ref - * [360a97] modify index - * [8026a7] modify files - * [f6e5d3] add new files to data - * [73d908] add img - * [2f270b] wiki docs - * [0dcb84] modified - * [907aa3] change img - * [8a0bf2] add img - * [0ec1e7] add img - * [adeb98] new img - * [10976c] change img path - - [ b-angerer ] - * [b4e02d] Delete deploy_config.rst - * [160a6d] Delete deploy_install.rst - * [ad8745] Delete index.rst - * [cb2ef2] Delete installation_devmode.rst - * [3b663e] Delete permission_and_security.rst - * [6c15f0] Delete index.rst - - [ mdiener21 ] - * [8247e5] updated 2.0 structure and new files - * [74d40c] updated 2.0 structure and new files - - [ Barbara ] - * [6626c9] deleted - * [dc7bd1] test - * [cbe249] delete - * [ebac0f] new test - * [1f1c02] delete - * [206266] add images - * [9f0c72] new images - * [d8bdbb] correct spelling mistake - * [1482ff] replace security.rst - * [2e4564] change security.rst - - [ b-angerer ] - * [43e75a] Update security.rst - * [21d916] Update permission_and_security.rst - - [ mdiener21 ] - * [374ef6] documentation restructure and relink - * [397d3f] documentation tutorials restructure and relink - * [37264b] added images organizational other files - * [a7b7c2] moved folder /users/ to old_docs folder - * [79ce91] moved postgis, postgres install into folder /tutorials/admin/install plus line space error fixed - * [c1fc2b] updated to remove return char, moved one file to organizational - * [a2c7fd] moved reference docs to ref section - * [e99803] index update with references - * [046ff6] moved all docs into new folder newdocs - * [6ebf16] test doc - * [20df58] Merge branch 're_org_docs' of https://github.com/b-angerer/geonode into golfgis - * [957845] added new index organizational - - [ root ] - * [d7470b] add text - - [ mdiener21 ] - * [5734ab] move _ext into newdocs - * [0955a2] added _ext files - * [275159] remove api.txt problem, added todo extension sphinx - - [ Barbara ] - * [e08ce3] modify conf.py - * [7fe55e] modified index - - [ root ] - * [a3184d] add index - * [c81973] add folder contribute - * [ef8a9b] add files - * [e33305] changes - * [614997] deleted - * [3ca23a] deleted - * [f13b11] delete - * [35bae7] deleted - * [26de00] deleted - * [60a1f3] deleted - * [cdfc42] modify - * [943d5f] modify - * [c3123b] modify - * [12fc5d] modify - * [8ece91] modify - * [f84aa6] modify - * [21b9fd] modify - * [edbd6c] modified - - [ Barbara ] - * [4976e9] deleted - - [ root ] - * [a78160] modified - * [395c97] modified - - [ Barbara ] - * [0eaa6d] deleted - - [ root ] - * [71a050] modified - * [77925a] modified - - [ mdiener21 ] - * [cea114] lots of fixes to toc doctree errors and warnings, text corrections and more - * [9b1b3c] remove empty folder moreinfo in tutorial users with new link to organizational how to contribute - - [ Simone Dalmasso ] - * [cf3b27] add the alternative path to raty - - [ capooti ] - * [70cc24] Added some more static files, generated by paver update_static, to .gitignore. - - [ Simone Dalmasso ] - * [a6b342] put rate stars even if there are less that 5 items - - [ capooti ] - * [8ab588] This fixes #975. It avoids that a new instance of an existing profile external to GeoNode is created after saving again the form. - - [ Simone Dalmasso ] - * [d6b89e] rate existing in in any case, avoids possible unrated items - - [ capooti ] - * [1bb6fc] Fixed the profile about item template. - - [ mdiener21 ] - * [b1119b] added guidelines how to write documentation and about - * [a85ead] lots of index toctree corrections and links within the docs corrected - - [ capooti ] - * [8a8584] Remove the 'Who can edit this data?' div if in the upload are there are only rasters. Refs #603. - * [5f8e52] The select user ajax view now search, with the username, the individual and organization name as well. - - [ mdiener21 ] - * [70b6a1] removed .idea files - * [658182] fixed remaining warnings duplicate lables rst errors - - [ Jeffrey Johnson ] - * [f4eaa5] some rearrange/cleanup - * [575979] Moving things around back to normal docs dir - * [451ad9] Update version in conf.py - * [87fca3] remove old docs - * [32ec47] remove errant new_docs in master - * [5e5d76] clean add of old docs from master - * [26435c] Removing _ext (not used here) - * [7d3d6d] Moving docs stuff around - * [54f777] Moving more docs stuff around - * [193470] Remove .idea - * [ef48d6] Move errant local_settings.py to local_settings.py.sample - * [11e7c3] small docs changes - - [ capooti ] - * [498c34] Slightly improved some admin pages. - - [ Christian Spanring ] - * [582fd5] re-introduces jquery raty to static css and js assets. resolves #1073 - - [ Jeffrey Johnson ] - * [95a8c6] removing duplicate rst docs - * [8b54b9] Move docs_old under old dirs in docs tree - * [181185] move stuff out of docs/organizational/old - * [b43e2b] move things out of docs/tutorials/admin/old - * [166868] edits to docs/index.txt - * [7f7832] edits to docs/index.txt - * [47795b] More updates to index.html - * [0362be] Remove doc path from tutorials/index - * [b11303] minor edit to docs/index.txt - * [3cd338] Moving more stuff around and some edits - * [cce362] More docs changes - * [292daf] Remove duplicate overview - * [ca5a04] remove section about data for users workshop - * [f41840] more small docs tweaks - * [168b2b] more docs tweaks to get rid of warnings etc - * [318461] Fix failing test - * [9c4ed5] Finish fixing test - * [bf343d] Add javascript to reference/index.txt - * [6e3236] Remove _templates from workshops - * [c88e0d] Initial i18n setup for docs - * [dda7bd] Update spanish from transifex - * [1b2694] Update after makemessages - * [8549d0] Update from transifex - * [739cbd] Update after compilemessages - * [ad92f7] Small change in russian from transifex - * [ec5dfb] Further work on docs i18n setup - * [f82049] Initial add docs i18n - * [5d244e] add docs/i18n/pot/.doctrees/ to .gitignore - * [21a02b] Remove pot dir from tree - * [b0dc14] adding zh_CN to docs i18n - * [a626bc] Update es docs from transifex - - [ capooti ] - * [422a64] Updated README file with instructions to build the documentation. - - [ Christian Spanring ] - * [f7ca71] updates jquery raty depency to version 2.5.2 - * [55f406] change to non-minified version of jquery.ajaxQueue in gruntfile - - [ Tyler Garner ] - * [7003bc] Fixes issue #1086. - - [ Ariel Nunez ] - - -- Ariel Nunez Thu, 05 Sep 2013 15:21:23 +0000 - -geonode (2.0.0+beta52) precise; urgency=high - - * [9ed9c9] Fixed typo in DATASTORE retrieval - - -- Ariel Nunez Sat, 31 Aug 2013 01:50:54 +0000 - -geonode (2.0.0+beta51) precise; urgency=high - - * [2a5371] Cosmetic fixes in layer upload page - * [466e58] Added animaget overlay from jquery ui progress bar - * [c099da] Added jquery-ui.css to .gitignore - - -- Ariel Nunez Fri, 30 Aug 2013 21:52:26 +0000 - -geonode (2.0.0+beta50) precise; urgency=high - - [ Tyler Garner ] - * [39f4ea] Updated local_settings.py samples. - - [ Ariel Nunez ] - * [021281] Disable GEOGIT by default - * [d521f9] Improved handling of unicode files with non ascii characters. refs #1017 - * [40aa92] Added changelog commit to publish paver task - * [8348a8] Updated changelog - * [735142] Added better error message for non ascii encoding.\nThis only works in the development server and does not seem to work in apache - * [f35f34] Added missing quotation mark in pavement.py - * [9cca0d] Removed mention of 2.0b50 - - -- Ariel Nunez Fri, 30 Aug 2013 21:28:39 +0000 - -geonode (2.0.0+beta49) precise; urgency=high - - [ vdeparday ] - * [e57c92] Create /updatelayers hook, reformat output of gs_slurp to avoid code duplication - - [ Simone Dalmasso ] - * [0400c0] use another way to check if the datastore is enabled in layer_edit_check - - [ capooti ] - * [e16958] Now paver will install GeoNode with Python 2.6 too. - * [e04557] Added the basic instructions to run tests after installing GeoNode. - - [ vdeparday ] - * [91a382] Modify updatelayers command version to match the new output of gs_slurp - * [d5b17f] Require to be superuser to run updatelayers - - [ Tyler Garner ] - * [d850b8] Created a OGC_Servers_Handler and OGC_Server class to make dealing with OGC Server settings less painful. - - [ Ariel Nunez ] - * [50cbce] Updated .gitignore - * [af7339] Updated OGC_SERVER settings in local_settings.py - * [da1a91] Updated changelog - - -- Ariel Nunez Fri, 30 Aug 2013 20:22:53 +0000 - -geonode (2.0.0+beta48) precise; urgency=high - - * [ce0b6a] Harmonized source and production in gruntfile - * [145e11] Updated changelog - * [84fe76] Updated minified javascript to include jqueryui - - -- Ariel Nunez Fri, 30 Aug 2013 02:19:49 +0000 - -geonode (2.0.0+beta47) precise; urgency=high - - * [3be4ed] Updated German, Spanish and Italian translations - * [284eed] Added some portuguese translations - * [962acf] Updated translations - * [1be776] Removed files that are automatically created and were ignored before - * [6bbf9d] Added minified files to the repository - * [1a5e6a] Added ajax progress to gruntfile - * [8b1830] Updated assets.min.js - * [d2d6e6] Updated changelog - - -- Ariel Nunez Fri, 30 Aug 2013 01:05:13 +0000 - -geonode (2.0.0+beta46) precise; urgency=high - - [ Simone Dalmasso ] - * [3b9f2e] add basic charset choice to the upload form - * [2d982e] handle charset on layer save - - [ Ariel Nunez ] - * [6daa73] Added charset option to create_featurestore - * [12d4c0] Added charset encoding to save method - - [ b-angerer ] - * [762f09] Update AUTHORS - - [ Ian Schneider ] - * [674fd5] remove curly dangling bracket - - [ Tyler Garner ] - * [361300] Encode strings that may receive non-ascii characters to utf-8. - - [ Ariel Nunez ] - * [6ee099] Get charset from upload form. - * [c33428] Fixed merge conflict - * [221bee] Added russian .po - * [754749] Added missing .mo files - * [ee0572] Added DEBUG_STATIC=False to default settings - * [b8ff7f] Updated changelog - - -- Ariel Nunez Thu, 29 Aug 2013 22:10:57 +0000 - -geonode (2.0.0+beta45) precise; urgency=high - - [ Jeffrey Johnson ] - * [64b9b9] Initial work on progress in upload - * [8f3637] More work to make upload progress work with rest based uploads - * [1aeb6f] Update javascript dependencies - * [7788ba] Rework progress function - * [209f3d] Adding ajax queue - * [b44d3f] Adding ajax queue to base template in debug - * [bc21ca] Use ajaxQueue in layer upload - * [fd4435] Fix polling setup - - [ Michael Diener ] - * [162b3e] added Michael Diener to authors - - [ Tyler Garner ] - * [42fac6] Updated acls and resolve_user functions to use the user's profile setting. - - [ Ariel Nunez ] - * [b1aba8] Updated changelog - * [955c8b] Updated geoserver url in local_settings - * [4c6c47] Updated compiled libs - * [dcb1b9] Updated settings and local_settings to set DEBUG_STATIC to False - - -- Ariel Nunez Thu, 29 Aug 2013 12:19:19 +0000 - -geonode (2.0.0+beta42) precise; urgency=high - - [ capooti ] - * [f8dc5e] Moved resourcebase_info_panel.html to its proper path. - - [ Simone Dalmasso ] - * [27c527] use just 'postgis' instead of the full engine in _create_db_featurestore - - [ capooti ] - * [e3a4a9] Fixes several broken metadata attributes in layer info page. - - [ Ariel Nunez ] - * [d9c20e] Updated changelog - - -- Ariel Nunez Wed, 28 Aug 2013 12:37:54 +0000 - -geonode (2.0.0+beta41) precise; urgency=high - - [ Ariel Nunez ] - * [1f303c] Updated changelog - - [ Tyler Garner ] - * [805bdb] Reverted previous changes that were unnecessary. - - [ Ariel Nunez ] - - -- Ariel Nunez Wed, 28 Aug 2013 02:35:20 +0000 - -geonode (2.0.0+beta40) precise; urgency=high - - [ Tyler Garner ] - * [8fe926] Fixes to make both the rest and importers work again. - - [ Ariel Nunez ] - * [433dd6] Updated changelog - - -- Ariel Nunez Tue, 27 Aug 2013 21:56:20 +0000 - -geonode (2.0.0+beta39) precise; urgency=high - - [ Ian Schneider ] - * [a0209e] follow up to c4dd8d9e, fix slop and add test - - [ Tyler Garner ] - * [1387ab] Updated the GEOGIT_ENABLED and TIME_ENABLED items in resource_urls to use the UPLOADER setting dict. - - [ Matthew Hanson ] - * [a3c732] fixed bug with datastore upload - - [ Ariel Nunez ] - * [5d610e] Updated changelog - - -- Ariel Nunez Tue, 27 Aug 2013 19:05:41 +0000 - -geonode (2.0.0+beta38) precise; urgency=high - - * [6bf2f6] Fixed references to user and password in geoserver helpers - * [d0f3c4] Comment out delete_from_postgis because it was part of a try/except block that was too general - * [7553c4] Updated changelog - - -- Ariel Nunez Tue, 27 Aug 2013 16:00:08 +0000 - -geonode (2.0.0+beta37) precise; urgency=high - - [ Simone Dalmasso ] - * [45ee45] indent with spaces in pavement - * [13c80d] add missing settings in utils - * [70fd79] read the missing indentation - - [ Ariel Nunez ] - * [5dc0f1] Updated changelog - * [a8ba91] Potential fixes for the missing thumbnails problem - - [ Tyler Garner ] - * [ca9bb3] Fixes _ASYNC_UPLOAD function to work with new settings. - - [ Ariel Nunez ] - * [2f828a] Enable saving shapefiles to postgis by default in packages, refs #1035 - - -- Ariel Nunez Tue, 27 Aug 2013 14:40:27 +0000 - -geonode (2.0.0+beta35) precise; urgency=high - - * [a2d5fb] Added thumbnail creation fix to install.sh - * [e1cb72] updated changelog - - -- Ariel Nunez Tue, 27 Aug 2013 03:14:33 +0000 - -geonode (2.0.0+beta34) precise; urgency=high - - * [03b3ac] Added missing comma to local_settings in packages - * [740ecf] Released 2.0b33 - - -- Ariel Nunez Tue, 27 Aug 2013 02:37:29 +0000 - -geonode (2.0.0+beta33) precise; urgency=high - - [ Ian Schneider ] - * [dd77ae] fix for resolve_user w/ invalid credentials - - [ Tyler Garner ] - * [b69282] Added the layer_resolve_user url to the login required whitelist. - - [ Ariel Nunez ] - * [f18a19] Added tuple with possible charset encodings to enumerations.py - * [5c680a] Tuples are supposed to be tuples, not lists. Thanks @tomkralidis. - - [ Tyler Garner ] - * [be8687] Added UTF-8 encoding to the str class on the Style model. - - [ Matthew Hanson ] - * [7b7082] moved DB_DATASTORE parameters into DATABASES dictionary; turn on/off through OGC_SERVER DATASTORE option - - [ Jeffrey Johnson ] - * [df31a5] Use more verbosity in the install_sample_data fab task - - [ Matthew Hanson ] - * [1b3b6e] Fixed OGC_SERVER context variables that were being overwritten by default - - [ Jeffrey Johnson ] - * [521d26] Fix one more reference to GEOSERVER_BASE_URL - - [ Ariel Nunez ] - * [eeb3fc] Updated changelog - - -- Ariel Nunez Tue, 27 Aug 2013 01:31:44 +0000 - -geonode (2.0.0+beta32) precise; urgency=high - - [ Jeffrey Johnson ] - * [0ab954] Fix alignment of action buttons on layer detail - * [99fd1a] Refactoring of GeoServer into OGC_SERVER settings and other touchup in settings.py - * [3392e3] Change settings.GEOSERVER_BASE_URL to settings.OGC_SERVER['default']['LOCATION'] - * [edf958] Change ref to GEOSERVER_BASE_URL in pavement.py - * [1825c4] Switch GEOSERVER_CREDENTIALS to use new OGC_SERVER dict from settings.py - * [efb114] Fix OGC_SERVER reference in MAP_BASELAYERS - * [0ff494] Fix missing : in geonode/layers/views.py - * [dfc218] Fix typo in geonode/maps/models.py - * [3f58bc] Temp fix in geonode/context_processors.py until @ingenieroariel fixes this for realz - * [db86c6] Adjust to changes in DB_DATASTORE in settings.py - * [1ce752] More temp fixes in context_processors.py - * [4dd097] Fix DB_DATASTORE references - * [1e1969] Hide perms widgets if GS_SECURITY_ENABLED = False - * [500d36] set printService to empty string if MapFish is not enabled - - [ Tyler Garner ] - * [ca93ab] Changed the upload function to use the case insensitive get_files function vs shp_files. - - [ Matt Bertrand ] - * [d896c7] Fix layer attribute form on metadata page - * [9317c3] Handle invalid attribute form - - [ Jeffrey Johnson ] - * [7083c6] Switch bower to grunt-cli for travis - * [1c4f4f] Switch bower to grunt-cli for travis - - [ Ariel Nunez ] - * [f3af7e] API design document moved to GNIP as with number #1042 - - [ Tyler Garner ] - * [b81208] Allow user to select destination GeoGit repository. - - [ Simone Dalmasso ] - * [85c323] initial work on documents respecting permissions - - [ Tyler Garner ] - * [cc78cb] Provides new middleware that allows administrators to require users to be authenticated before accessing a majority of the application. - - [ Simone Dalmasso ] - * [069363] adjust the db_datastore lookup - * [e62f13] enforce the check on the permissions structure - * [bf74fc] remove the inline contactrole from layer admin and the custom action - * [034d5f] don't assign the owner as poc and metadata_author if they already exist - * [91061d] move import to the top of the page - * [c85f03] hide counts and thumbs in maps and docs metadata - - [ Tyler Garner ] - * [2a5d1a] Ugly fix to make the LoginRequiredMiddleware test case work correctly. - * [a98f82] Added documentation for the new LoginRequiredMiddleware settings. - - [ Ian Schneider ] - * [9d7940] user w/ specific perm unable to see in search - - [ Simone Dalmasso ] - * [409e0d] fix error in dependency - * [b06de8] use 403 to benefit from redirect - - [ Ian Schneider ] - * [c4dd8d] add fullname and email to acls - - [ Jeffrey Johnson ] - * [2f00b3] Use DB_DATASTORE dict - - [ Tyler Garner ] - * [51a329] Forced UTF-8 encoding on strings that may receive unicode input. Fixes character encoding problems in updatelayers and in the attributes model. - - [ Ariel Nunez ] - * [a2affc] Added dependency on python-django-downloadview for debian package - * [7a65f7] Added changelog' - - -- Ariel Nunez Fri, 23 Aug 2013 21:25:23 +0000 - -geonode (2.0.0+beta31) precise; urgency=high - - [ Simone Dalmasso ] - * [fee8f2] first draft on wcs link creation - - [ Paolo Pasquali ] - * [8c445b] New list/grid doc, layer and map layout (to be fixed). Add page-title and space word-wrap - * [3affea] Profile grid/list new layout - * [5e7073] Search list with thumbnail redesigned - * [a1adb6] Moved comments h3 inline style to base.less - * [ad91ad] Comments CSS fixes - * [7bc15f] Fix for multiple comments - - [ Ariel Nunez ] - * [0f9cd0] Better 500 error - * [603d5b] Committed changelog - - [ Paolo Pasquali ] - * [b16a4e] Add simple text if user is not logged in - * [f34a08] Remove Paolo Corti duplication and correct username - * [1674e1] Fix closing tag - * [a3b4b7] Home page latest maps css fix - * [ef3b34] Fix the about section css and grid search page css. Minor fix in comments. - * [4ef97c] Fix list layout (must be improved) - - [ Simone Dalmasso ] - * [5e7f74] prepare document detail for metadata links - - [ Paolo Pasquali ] - * [63a2c8] Minor fix in page title - - [ Simone Dalmasso ] - * [6c756c] don't hide page numbers, can break pagination - - [ Paolo Pasquali ] - * [3ea30f] Minor fix in Activity feed - - [ Bart van den Eijnden ] - * [0a8a14] automatically create a thumbnail when people save their map in the composer - - [ Paolo Pasquali ] - * [54a7a8] Add badge to keyword search - - [ Simone Dalmasso ] - * [b89c05] move map view to the top in map detail - - [ Paolo Pasquali ] - * [fb12f6] Minor fixes in map and profile layout - - [ Micah Wengren ] - * [267357] Remove block 'extra_head' from template layer_upload.html - * [0f262d] reverted pavement.py - - [ Paolo Pasquali ] - * [3409d5] Fix to Login page Template - * [891d13] Fixing Profile Edit page template - * [680f39] Restore profile edit title - * [38efb7] Add links to data list details - - [ Tom Kralidis ] - * [89e911] fix LogoURL href [ci skip] - - [ Simone Dalmasso ] - * [25f16d] connect the catalogue to the document save - * [0e9d88] remove the unwanted tag - * [f59615] fix documents models - * [2fc827] update search tests to reflect the new documents global bbox - - [ Ariel Nunez ] - * [0932aa] Updated install script from svn - - [ b-angerer ] - * [ebd9ac] Create contribute_docu.md - * [320c24] Update contribute_docu.md - * [231988] Update contribute_docu.md - * [bd2b0f] Create contribute_to_docu - * [cb9b5e] Delete contribute_to_docu - * [bb027a] Create index.txt - * [f04793] Create contribute_docu.md - * [f15603] Update contribute_docu.md - * [662e81] Create install_postgresql.md - * [333b22] Update install_postgresql.md - * [0049f8] Update install_postgresql.md - * [9c7e54] Create install_postgis.md - * [7f0a38] Create install_geonode_in_devmode.md - * [52ad35] Update install_geonode_in_devmode.md - - [ Angelos Tzotsos ] - * [421ce3] sync install script with OSGeoLive 7.0beta1 - - [ b-angerer ] - * [c9eafe] Update contribute_docu.md - * [1af2bb] Update install_geonode_in_devmode.md - * [a7cf1a] Create config_geonode_dev.md - * [3d82e2] Update install_postgresql.md - * [a7b3e5] Update install_postgis.md - * [1082a5] Update install_postgis.md - * [2888e1] Update index.txt - * [ad63cd] Create test.rst - * [8e21e8] Create testmd.md - * [020435] Update contribute_docu.md - * [285413] Update contribute_docu.md - - [ Syrus ] - * [902046] when uploading a layer, it might take longer than 4 seconds in which case the previous code would add the layer to geoserver but all steps that followed in geocode would fail. this code checks every 2 seconds and waits as long as 1 min for upload to complete. needed for slower server, busy server, or larger data. - - [ b-angerer ] - * [5f7a86] Added images - * [bf99d1] Create contribute_docu.rst - * [823ea3] Update contribute_docu.rst - * [0da9b9] new images for contrib_docu - * [c54cd1] add image - * [bf23d9] add images - * [ff9a09] add images - * [22a864] Update contribute_docu.rst - * [d4b3d7] Update contribute_docu.rst - * [857ea7] Update contribute_docu.rst - * [50ce04] Update contribute_docu.rst - * [981349] Update contribute_docu.rst - * [1b1e7d] Update contribute_docu.rst - * [7c56bf] Create contribute.rst - * [8a601c] Update contribute.txt - - [ Jeffrey Johnson ] - * [2ed8ea] Changes made by new bower version (also fix new raty path) - * [9bf049] Update bower.json to peg jquery-ui version - * [f49011] Update base.css - * [4d49af] remove hack to remove jquery-timago recursive symlink (no longer needed) - - [ b-angerer ] - * [a110d6] Delete contribute_docu.md - * [a325e0] Delete index.txt - * [3f6e18] Delete test.rst - * [c62c19] Delete testmd.md - * [ede27c] Delete contribute_docu.md - * [9bd181] Create install_devmode.rst - * [662ada] Create contribute_docu.rst - * [74484c] new images - * [f6f646] Update contribute_docu.rst - * [5bc392] new image - * [b23091] Update install_devmode.rst - - [ Jeffrey Johnson ] - * [c443b9] Add bower-installer config for bootstrap-datepicker - * [2c67bc] Fix qunit path in bower.json - - [ Christian Spanring ] - * [1182ba] starting grunt integration - * [5973e7] updates readme with grunt-cli install instructions - * [475773] removes orphaned CSS blocks from base and 500 templates - * [9f645f] removes bower and grunt dependency from package.json - * [1bab4e] updates grunt configuration - * [6f7918] adds DEBUG_STATIC condition for assets in base template - * [594c1c] adds production-ready assets to repository - - [ b-angerer ] - * [20f405] Update config_geonode_dev.md - * [a59ca7] Update install_postgresql.md - * [95cef0] Update install_postgresql.md - * [bc4c2a] Update install_postgresql.md - * [71245e] Update install_postgresql.md - * [409dcb] Update contribute.txt - - [ Angelos Tzotsos ] - * [b78182] fix for postgres issue and permissions #938 - * [e06eca] second try: fix for postgres issue and permissions #938 - * [66c54b] third try: fix user permissionfor postgres #938 - * [473b23] added host to local_settings.py #938 - - [ Christian Spanring ] - * [74ea14] updates gruntfile with improved bootstrap build rules - - [ Angelos Tzotsos ] - * [d5aa4d] starting GeoServer during build to update layers #938 - * [e5ff54] added start/stop scripts #938 - * [c6453a] minor fixes #938 - * [93dc88] give more time to GeoServer to start-up (#938) - - [ b-angerer ] - * [05020d] Update contribute.rst - * [f26056] Update contribute_docu.rst - * [285010] Update contribute.rst - * [e4c054] Update contribute.txt - * [ea2a18] Create contribute_docu.txt - * [e35997] Update contribute_docu.rst - * [c05981] Update contribute_docu.rst - * [f56e27] Update install_devmode.rst - * [995ab6] Update contribute_docu.rst - - [ Christian Spanring ] - * [672dd3] bootstrap re-organization - * [0b59ee] .gitignore - * [ae3ec6] adds bootstrap-datepicker to grunt build process - * [5f154f] adds require.js to grunt build process - * [aeb71a] adds jquery-tinysort to grunt build process - * [568fe9] adds jquery-raty to grunt build process - * [a51491] adds jquery-waypoints to grunt build process - * [d4b2e7] adds all "other" assets from bower-installer to gruntfile - * [931406] updates qunit testrunner with new lib location - * [5fa89b] updates production-ready static js and css assets - * [d28822] removes grunt dependency from paver setup_static task - * [385f43] removes setup_static paver tastk from setup_task - * [de2ec3] adds bower and grunt dependency to static files node package - - [ Ariel Nunez ] - * [886a3e] Moved gisdata to Recommends, refs #1015 - - [ b-angerer ] - * [39daad] Update contribute_docu.rst - * [c5ef60] Update install_devmode.rst - * [5d0736] Create permissions.rst - - [ capooti ] - * [4df326] Removed the reference to the unneeded METADATA_DOWNLOAD_ALLOWS in the layer info template. - - [ b-angerer ] - * [d7bc2b] Update permissions.rst - * [cc8165] Create tutorial_security.rst - * [aeb7e6] Update tutorial_security.rst - * [078f55] Update tutorial_security.rst - * [37b555] Update tutorial_security.rst - - [ Tyler Garner ] - * [2d2bc2] Add the map object to the map_view response. - * [a00dfb] Fixes typo in cloud deployment script. - - [ Jeffrey Johnson ] - * [2c28ad] Rebuild base.css - * [d11b54] Remove committed libs - * [5e6789] Adding minified assets - * [d4443a] update base.css again - * [0c4f54] add images from dependencies - - [ Christian Spanring ] - * [3438d1] integrates rate.js with global_search.js - * [643ab8] adds jquery.raty to assets.min.js - * [44ea52] adds waypoints.js to assets.min.js - * [0622b7] removes require.js dependency from global_search.js - * [1c7838] removes hogan.js - * [1a098d] removes minified waypoints.js from grunt - * [d1e993] replaces jQuery UI with Bootstrap progressbar - * [ce8648] removes jQuery UI from bower/grunt - * [17606d] removes jQuery minicolors from bower/grunt - * [45b58a] fixes duplicate geonode_auth template tags loader - * [e0fafb] adds multi-select to assets.min.js and fixes some inline javascript on layer_style_manage template. - * [6265d9] removes wysihtml5 from bower/grunt - * [0e74cf] minor gruntfile re-organization - * [712f38] removes loadmore from bower/grunt - * [4512d2] adds client-dependencies for uploader to repository - * [2e0c2d] adds missing select2 images to repo. closes #1023 - * [0a6d7c] adds regex to grunt and fixes CSS image urls - * [59ad45] remove obsolete css/*.png|gif - * [4d6ece] adds lib/img/spinner.gif to repository - * [48e551] add spinner.gif to gruntfile - * [95917c] cleanup: removes obsolete bower-installer config - - [ Tyler Garner ] - * [f0281a] Safely check settings for DEBUG_SETTINGS. - - [ Ariel Nunez ] - * [880685] Running paver update_static changed the files in my local checkout. I am committing them assuming they are newer - - [ Tyler Garner ] - * [94154a] The post_delete signal should only delete the default_style if the style is not linked to any other layers. - - [ Ariel Nunez ] - * [a9183a] Ask user to use ascii characters in the upload.,refs #989 - - [ Tyler Garner ] - * [4b73f6] Fixed test failure and improved post_delete_layer logic. - - [ Jeffrey Johnson ] - * [f27cd6] Update base.css and assets.min.js after paver update_static - - [ Ariel Nunez ] - * [3212c4] Log uploader exceptions aside from passing them back to the user - * [7bcae3] Removed pip instal imaging from OSX setup instructions - - -- Ariel Nunez Fri, 09 Aug 2013 22:10:31 +0000 - -geonode (2.0.0+beta29) precise; urgency=high - - [ Paolo Pasquali ] - * [241654] Disabling Less minify option - * [49fd32] Merge branch 'master' of https://github.com/GeoNode/geonode - * [c62dfd] Added myself to AUTHORS - * [9b111e] Added badges to Type Filters - * [2b7649] Removing comment - - [ Ariel Nunez ] - * [99ee3a] Made drop target easier to find - * [4deb9a] Simplified smaple data installation in fabfile - * [7120bc] Updated changelog - * [166eba] Added missing command to import data in fabfile - - [ Dennis Wilhelm ] - * [ad2090] Extended the german translation - - [ Ariel Núñez ] - * [a63e9c] Added tyler, dwilhelm89, angelos, and paolo corti to AUTHORS. - * [55a676] Added travis, nolan, dave, and chris to authors - - [ Ariel Nunez ] - * [ecab73] Fixed thumbs dir permission problem - * [b814f1] Updated django-geoexplorer - * [d66849] Test showing usage of layer permissions, this may need to be updated around the codebase - - [ Jeffrey Johnson ] - * [b58390] Remove trailing slash in layer_upload_time.html template - * [748267] Fix up require config in srs.js and time.js - * [a8a9f2] Format Continue button in LayerInfo - * [e56130] use global jquery - * [630ca9] Handle for multiple time layers more effectively - - [ Simone Dalmasso ] - * [817e50] fix permissions name in templates - - [ Matthew Hanson ] - * [dbe348] added blank=True so admin fields aren't created as being required - - [ Simone Dalmasso ] - * [71b4b7] Clear any orphan thumbnail before saving a new one - * [9e44e5] Delete the thumbnail on the resource base delete - - [ Ariel Nunez ] - - -- Ariel Nunez Fri, 12 Jul 2013 23:05:00 +0000 - -geonode (2.0.0+beta27) precise; urgency=high - - * [e69be0] Added dateutil and updated changelog - * [f8f666] Fixed typo in geonodedir location in installer.sh - * [7656d3] Added back --all to syncdb - - -- Ariel Nunez Fri, 05 Jul 2013 07:47:47 +0000 - -geonode (2.0.0+beta25) precise; urgency=high - - * [c77243] Added pinax-theme-boostrap to the debian/control file - * [650612] Updated changelog - * [658c4e] Added another missing pinax package - * [571d64] Added missing dependencies to control file - - -- Ariel Nunez Fri, 05 Jul 2013 05:41:27 +0000 - -geonode (2.0.0+beta24) precise; urgency=high - - * [62cc15] Fixed path for local_settings.py symlink - * [ea051a] Updated changelog - * [0a14d9] Added python-django-announcements to the debian/control file - - -- Ariel Nunez Fri, 05 Jul 2013 04:44:39 +0000 - -geonode (2.0.0+beta22) precise; urgency=high - - [ Angelos Tzotsos ] - * [c78c0e] sync osgeolive script to osgeo svn - * [a3b458] sync with osgeolive svn - - [ Tyler Garner ] - * [aaaf87] Let align attributes headers. - * [56f047] Added bower installation instructions to Mac OSX section. - * [11c544] Cleaned up the comment header in the layer detail view. - * [e37e11] Made map_detail page consistent with layer_detail page. - - [ Ariel Nunez ] - * [bb7d7c] Updated changelog - * [41fc3f] Troubleshooted postgres database creation under vagrant. Specified locale and template to make it work. - - -- Ariel Nunez Fri, 05 Jul 2013 04:05:51 +0000 - -geonode (2.0.0+beta21) precise; urgency=high - - * [9ebf02] Added locale option to postgis database creation - * [9bb881] Updated changelog - * [bb863f] Simplified installation now that we have native debian packages - - -- Ariel Nunez Thu, 04 Jul 2013 17:33:49 +0000 - -geonode (2.0.0+beta19) precise; urgency=high - - [ Paolo Pasquali ] - * [bdcd3a] Minor CSS fixes - - [ Simone Dalmasso ] - * [889213] add map view button in list item - * [ddfca6] hide pagination numbers - - [ Paolo Pasquali ] - * [9efdd9] Added some CSS to Layer Style Manage - * [5e2b52] No minify option in less - - [ Simone Dalmasso ] - * [acce07] show again the page numbers as it breaks pagination - - [ Ariel Nunez ] - * [0daaa1] Added python-dev to the list of installed packages. This is needed because of updates to the shapely version that end up requiring to compile pyproj and others - * [3ac295] Added python requirements to debian control file too - - -- Ariel Nunez Thu, 04 Jul 2013 14:55:34 +0000 - -geonode (2.0.0+beta18) precise; urgency=high - - * UNRELEASED - - -- Ariel Nunez Thu, 04 Jul 2013 06:09:05 +0000 - -geonode (2.0.0+beta17) precise; urgency=high - - * [3e9b38] Fixed name in setup.py that should not have been changed - - -- Ariel Nunez Thu, 04 Jul 2013 04:42:15 +0000 - -geonode (2.0.0+beta16) precise; urgency=high - - * [b3bcae] Updated changelog - - -- Ariel Nunez Thu, 04 Jul 2013 04:36:28 +0000 - -geonode (2.0.0+beta15) precise; urgency=high - - [ Simone Dalmasso ] - * [8f5c24] add missing migration for categories - - [ Jeffrey Johnson ] - * [7d2dc4] Bump pinax-theme-bootstrap to 3.0all - - [ Ariel Nunez ] - * [3c144f] Upgraded dependencies for django third party apps - * [db3967] Changed name of distribution to python-geonode - * [9f8742] Updated changelog - - -- Ariel Nunez Thu, 04 Jul 2013 03:59:07 +0000 - -geonode (2.0.0+beta13) precise; urgency=high - - [ capooti ] - * [8d9dcb] Removed the METADATA_DOWNLOAD_ALLOWS stuff - - [ Paolo Pasquali ] - * [d28f71] CSS rating fix - - [ Simone Dalmasso ] - * [ac0604] initial work on query replace - * [768e87] adjust the server response to the new uploader in layer-replace - * [f707ab] update the integration test to reflect the new error code - - [ Tom Kralidis ] - * [1adfc7] bump pycsw to 1.6.0, atom/dif/fgdc are now default profiles so remove from config - - [ Angelos Tzotsos ] - * [40f1a0] icon fix for osgeolive - - [ Paolo Pasquali ] - * [572657] Added some css to Profile Detail - * [c0224a] More CSS in Profile detail - - [ Ariel Nunez ] - * [899afb] Added utf8 encoding to local_settings, refs #974 - * [55b1b3] Updated changelog - - -- Ariel Nunez Tue, 02 Jul 2013 15:06:34 +0000 - -geonode (2.0.0+beta12) precise; urgency=high - - [ Ariel Nunez ] - * [621fad] Updated changelog for beta11 - - [ Angelos Tzotsos ] - * [69f79c] minor fixes to OSGeoLive installation script - * [f1f38c] update year - * [8ff1c9] removed all dev packages during cleanup - - [ Ariel Nunez ] - - -- Ariel Nunez Mon, 01 Jul 2013 16:14:48 +0000 - -geonode (2.0.0+beta11) precise; urgency=high - - [ Jeffrey Johnson ] - * [a41ded] Initial (rough) add of docs/developers/api.txt and reference in docs/index.txt - * [f2f9b7] Remove static lib dependencies from GeoNode's source tree - * [0f7649] Add component.json and README to geonode/static - - [ capooti ] - * [38e228] Moved the SPATIAL_REPRESENTATION_TYPES base/enumerator to the SpatialRepresentationType model. - * [f3d301] Moved the CONSTRAINT_OPTIONS base/enumerator to the RestrictionCodeType model. Need to figure out if it is correctly exposed in full_metadata.xml. - * [e5758f] Moved the COUNTRIES base/enumerator to the Region model - - [ Paolo Pasquali ] - * [124c80] Many templates changes - * [874184] Removed bold from form label - * [d1121a] base css sync - * [455a04] Profile list redone - * [6e37cf] Fixed Document list img placeholder - * [438a26] Back to layer list - * [017411] Page title reduced - * [53d3d9] slide-pane text reduced - * [c26d37] Awesome icon color and size in item title - * [fd4930] Bold and size removed from form label text - * [83ed6a] Removed bold from h2, h3 and set @customFont to Body - * [75d8b9] Profile list layout fix - * [a38759] Explore people, profiles template fixed (but explore people still missing) - * [c5c977] navbar icons resized - * [6d4639] people evelope icon hover - * [bf2d8a] Fixed link to explore profile - - [ capooti ] - * [a45564] Added the METADATA_DOWNLOAD_ALLOWS setting - * [3f21a4] Added the gn_description and is_choice field, renamed slug to identifier and name to description (to have homogeneity with the other metadata models) in the TopicCategory model - * [596cbc] Removed the default value from resourcebase.category, as it is a nullable field - * [9929e2] Removing the get_default_category() method, as it is not needed anymore - - [ Paolo Pasquali ] - * [b9d14a] Grid view fixing in profile list page - - [ capooti ] - * [4edd9d] Using identifier for the layer.category metadata - * [ec8b6f] Now the metadata for resourceConstraints is correctly set to restriction_code_type.identifier - - [ Jeffrey Johnson ] - * [fc0abc] Add LayerStyleUploadForm - * [33950d] Add layer_style_upload view - * [26a2a0] Wire up layer_style_upload in layers/urls.py - * [cfd57c] Initial work on Layer Style Manager - * [226866] Hook the Style Manager Page up - * [e77a89] Remove yet another copy of jquery - - [ Paolo Pasquali ] - * [faa93f] Add Profile Base - * [23ed93] Added latest-maps class option in home page - * [f94593] Resizing the layer thumb - * [9f318e] layer list item template - * [c5fb48] map list item template - * [39a114] profile list item template - * [8410de] CSS update - * [d0ba3f] Index latest-maps class - * [cb6e02] profile follow html - * [de217b] Adding the profile list page - - [ Ariel Nunez ] - * [663799] Updated classifier - - [ Jeffrey Johnson ] - * [043c3d] Adding yet another jquery for now til we get bower sorted out - * [07d52d] Initial work to make geogit and time optional - * [99f98f] handle for GEOGIT_DATASTORE in feature_edit_check - * [9a8534] Skip time step if not selected - * [10789c] Add GEOGIT_DATASTORE and UPLOADER_SHOW_TIME_STEP to local_settings.py sample - * [4b80fb] Some cleanup - - [ Travis L Pinney ] - * [d94c6e] Added Mac OSX build instructions - - [ Simone Dalmasso ] - * [72b4d7] add update categories command - - [ Paolo Pasquali ] - * [ed6869] Reverting stars to orange - * [916596] Fixed double quotes in profile list item - * [956fd7] Back to no double quotes in _profile_list_item - * [4f533f] Adding padding between items - - [ Jeffrey Johnson ] - * [966245] Fix typo in geonode/layers/views.py (tests pass) - * [38451d] Reduce number of copies of jquery lib we have floating around - * [80fd19] Changes to feature_edit_check logic per @ischneider - * [36d771] Use store_type vs type - * [c928df] Set all optional settings to False in local_settings.py and add UPLOADER_BACKEND setting - * [ffd20d] Work to make the time and geogit boxes only show if those options are enabled - * [0abb99] Fix up syntax in template and rename passed vars - * [71fcc8] dont show the entire vector options div if the format is raster - * [e04c32] use !== when checking for raster type - * [805b7c] remove extraneous trailing comma - * [1e953b] Error handling for layer_style_manage - * [0c2d65] Add a Manage Styles option/button upon successful upload - * [db4399] Remove testing framework, LICENSE and README from lou-multi-select - * [e72395] remove jquery.multi-select.jquery.json - - [ Ariel Nunez ] - * [82496d] Added upload to pypi - - [ Jeffrey Johnson ] - * [8dbab7] Remove more libs - * [32a654] Remove even more libs - * [13c50b] add multi-select and qunit to component.json - * [016d75] add qunit and env-js to component.json - - [ Simone Dalmasso ] - * [ef856a] Move to the categories count method suggested by ischneider. Thanks @ischneider - - [ Jeffrey Johnson ] - * [81ef45] Initial pass of changing static url paths - * [ae4b2c] More replacements of libs with path to components - * [790465] some further fixups - - [ capooti ] - * [67e0e7] category.gn_description may be null (needed in migration). Updated the search page to use topiccategory.identifier instead than slug - - [ Paolo Pasquali ] - * [2b8c66] Layout minor bug fixes - - [ capooti ] - * [48e40b] Updated the gn_description of topiccategories to a shorter version in fixtures. - - [ Simone Dalmasso ] - * [30af60] fix some require paths for search - - [ capooti ] - * [6ca3e1] Now search for categories is using the identifier field (slug was replaced by identifier). - - [ Simone Dalmasso ] - * [d3650a] call again the utils on require load - * [a444e7] just use the global jquery - - [ Jeffrey Johnson ] - * [e2c1f6] rearrange dependencies in component.json for easier reading - - [ capooti ] - * [1f13b0] Added the attribute description field, to be used in metadata. Removed from now, until they will be implemented in gxp, the visible and display_order form fields in layer metadata page. - - [ Jeffrey Johnson ] - * [26eea9] more mods in component.json - * [abe462] Move to bower.json, use bower-installer and add resulting lib dir to .gitignore - * [d202c1] Put dependency main files in the right place in lib - * [7427e3] Move some static media around while we are in the process of updating paths. - * [7e858a] Add setup_static task to pavement.py - * [2c9b28] Some small updates to geonode/static/README - * [3c59bf] Some small updates to geonode/static/README - * [aab937] Move all extjs under the right dir - * [83a9aa] Update bootstrap path in base.less and rebuild base.css - * [19829f] Reenable use of geonode/static/geonode/Makefile - * [65d823] Handle for data_dir_zip not being found - * [77f3f3] Ass filter option and pass to gs_slurp - * [7fcc66] Set cache time to 0 from settings - * [feb0ce] Add options for filtering by store or by a filter string in updatelayers - - [ Tom Kralidis ] - * [b5be33] set distribution url if not set (#891) - * [6850c4] avoid double slashed urls - * [2e2316] emphasize XML upload possible lossiness (#138) - - [ Jeffrey Johnson ] - * [842673] Handle for data_dir_zip not being found - * [4cac40] Set cache time to 0 from settings - - [ Ariel Nunez ] - * [b1f757] Added freenode.txt for channel verification, will be removed soon - * [5e930c] Now our channel in Freenode is registered - - [ Jeffrey Johnson ] - * [b6e15b] Filter out layers explicitly disabled by geoserver in gs_slurp - * [678f59] Fail gracefully and log if the store cannot be deleted in cascading_delete - - [ Matthew Hanson ] - * [ed5441] removed initial bobby user - - [ Ariel Nunez ] - * [a32b19] Revert removal of bobby, it breaks unit tests - - [ Jeffrey Johnson ] - * [4103d2] Work from ROGUE on map thumbnails - * [bd63a5] Control show/hide of printNG based functionality - * [50436a] Add PRINTNG_ENABLED to geonode/local_settings.py.sample - * [bf48df] fixed footer - * [5ec3b8] Update i18n from transifex - * [81bdeb] makemessages/compilemessages - * [fd0314] another pull from transifex after pushing - * [eec142] add new langs - * [ce22d4] One more update - - [ Simone Dalmasso ] - * [e4a1dc] move counts to base tag - * [a0f1b6] fix tests - - [ capooti ] - * [fa7b17] Updated tests, as now we have a topiccategory.identifier field and not topiccategory.slug - * [a7f488] Merging code from the PR of @jj0hns0n. Thanks Jeff! - - [ Simone Dalmasso ] - * [712dee] remove ext in the form management and use modal forms - * [15838f] fix for maps and documents as well - - [ Jeffrey Johnson ] - * [5d4967] Better error handling for thumbnails - * [cfb602] Filter out profiles without a user attached (Person outside GeoNode) - * [65c3b1] rebuild base.css - * [737def] Fix up static paths to match geonode/static/lib - * [75b68b] Use .components dir - * [bef943] More work to get .components ignored - * [68d568] Fix path in base.less - * [e7ced1] Move the delete to the end of paver setup_static - - [ Ariel Nunez ] - * [8b1937] Testing script to install geonode on osgeo livedvd - * [f57378] Fixed geoservers port - - [ Jeffrey Johnson ] - * [96df38] More work to update paths for bower based install - * [bd4910] Fix bower config - * [162c11] Install dependencies for the travis build - * [1d755a] just less vs lessc - * [666a06] Install bower-installer - * [8ab1f9] Bump bootstrap version - * [ffed69] Disable running javascript tests temporarily - - [ Tyler Garner ] - * [c1e7d2] Fix for issue #638 and changed account page headers to be consistant with the rest of the application. - * [d29365] Fix for issue #638 and changed account page headers to be consistant with the rest of the application. - - [ Simone Dalmasso ] - * [02c132] fix less import path - * [1dbd8b] move scripts to the right block - * [f907a2] use the global jquery - * [9721ad] activate view count for layers and documents - - [ Jeffrey Johnson ] - * [f20c37] Add METADATA_DOWNLOAD_ALLOWS back to settings.py - * [0f5ad7] Fix footer - - [ Ariel Nunez ] - * [30d80d] Updates to install_geonode, refs #938 - - [ Tom Kralidis ] - * [5619da] Revert "store originally uploaded XML instead of marshalling to ISO for uploaded metadata" - * [f870e7] default is always ISO - * [f36515] move download_links to ResourceBase, extend LINK_TYPES - * [853bc8] do not utf8 encode metadata string - * [385473] do not advertise CSW metadata links within a metadata document itself - - [ Simone Dalmasso ] - * [1c8b35] remove the update categories command, no longer needed. - * [731bff] put back the default category - - [ Jeffrey Johnson ] - * [22bcb9] Set language for GeoExt - - [ Tom Kralidis ] - * [9e6fc4] how which field failed validation on metadata form (#138) - * [3cb936] show which field failed validation on metadata form (#138) - - [ Simone Dalmasso ] - * [2a19ac] use map layer title in map detail - * [ae7c5f] handle the special case .shp.xml metadata naming convention - - [ Ian Schneider ] - * [141b40] docs for geoserver dbclient configuration - - [ Paolo Pasquali ] - * [aea7d1] Layout fixes - - [ Simone Dalmasso ] - * [140243] replace spaces with underscore in the file name and selector - * [aa0480] put back keyword region as optional field - - [ Jeffrey Johnson ] - * [eef7d5] Remove select2.png from bower install paths - * [e738b8] Use store_type in call to create_geoserver_db_featurestore - * [8778e8] Fix syntax error in bower.json - - [ Simone Dalmasso ] - * [e16530] resources counts in profile list item - * [31c142] adjust counts in profile list item - - [ Matt Bertrand ] - * [d49cbb] Retain layer title sent in map json from viewer - - [ capooti ] - * [aa1e15] This fix several issues related to avatars (refs #637), but will need some design love - - [ Simone Dalmasso ] - * [79eacb] initial migration to jquery based - * [af70fd] move map download to jquery - - [ capooti ] - * [1ada1e] Updated instructions for Ubuntu to setup Node and tools required for static development - - [ Paolo Pasquali ] - * [0dedec] Adding Font-Awesome 3.2.1 support - * [e7bc00] Adding former variables.less changes into base.less - - [ capooti ] - * [ab08e9] Displaying other profile information in profile detail page (refs #621). Will need some more design love. - - [ Paolo Pasquali ] - * [9c932d] Only registered users can see people email - * [2760d4] Initial css fixing since bower - * [0ac2c7] Deleting wrong css path in initial css fixing since bower - * [65184b] Revert "Initial css fixing since bower" - * [28b3ec] Initial css fixing since bower - * [ef7476] Revert to @brown header - * [79af2c] Only registered users can see email in profiles page - * [386d46] A little CSS change - - [ Jeffrey Johnson ] - * [6c3445] Some layout cleanup in layer_detail.html - * [5dcdc9] Remove duplicate block in profile_detail.html - * [939b3f] Comment out print button in map_detail.html (until its hooked up with printNG) - - [ Ariel Nunez ] - * [7c493c] Fixes permissions on uploads. - * [8a6da0] Updated changelog for beta10 - - -- Ariel Nunez Sat, 29 Jun 2013 14:27:52 +0000 - -geonode (2.0.0+beta10) precise; urgency=high - - * [026d87] Updated changelog - * [b5bf06] Updated publish task to also tag releases - * [152924] Do not depend on ALLOWED_HOSTS being present before - - -- Ariel Nunez Sun, 16 Jun 2013 01:16:45 +0000 - -geonode (2.0.0+beta9) precise; urgency=high - - * [73deda] Added publish task - - -- Ariel Nunez Sat, 15 Jun 2013 16:44:09 +0000 - -geonode (2.0.0+beta6) precise; urgency=high - - [ vagrant ] - * [74de25] Removed temporary directory in debian package creation - - [ Ariel Nunez ] - * [122879] Updated changelog - * [1e207c] Revert last commit - * [f86e81] Change priority to high - - -- Ariel Nunez Sat, 15 Jun 2013 16:02:36 +0000 - -geonode (2.0.0+beta5) precise; urgency=high - - [ Jeffrey Johnson ] - * [f45cff] Add jquery requirement to FileType.js - * [6cf07a] Return 500 status code from _error_response - * [922531] Accept status code to json_response - * [9999dd] Handle for direct to final (for TIFF) - * [d1c3e2] Fixes in LayerInfo.js - * [1cf9f3] Refactor display of uploaded layer links and display as buttons - - [ vagrant ] - * [ff57b4] s/alpha/beta - - [ Ariel Nunez ] - * [826492] Set ALLOWED_HOSTS in geonode-updateip, refs #910 - - [ vagrant ] - * [f8f0d9] Update changelog - - -- Ariel Nunez Sat, 15 Jun 2013 15:57:17 +0000 - -geonode (2.0.0+beta4) precise; urgency=high - - [ Simone Dalmasso ] - * [599b9f] initial work on using the new uploader ui - - [ capooti ] - * [27171b] Some initial work for #601. - * [1f06d4] Now it is possible to display a layer style in the map. - - [ Ian Schneider ] - * [40f32e] initial work towards storing thumbnails - - [ capooti ] - * [9909f6] Removed some sample xml from comments. - * [ecdfb0] Now it is possible to download styles from layer info page. - - [ Simone Dalmasso ] - * [e38a35] make sure we tell the uploader js we are dealing with a layer - - [ Matt Bertrand ] - * [fbb794] Change 'Activities' to 'My Activities' and use trans templatetag - - [ capooti ] - * [ed8a1e] Added a couple of missing migration dependencies. - * [6bcce5] Removed the project migrations directory, it was there for some reason but it was not used. - * [4c4386] Removed this initial_data file as it is already in people/fixtures. - * [d8fc2e] Added the initial migration for the base and documents apps. - * [524ce6] Updated migrations for layers and maps apps. - * [11b2fc] Moved the people initial_data fixtures to the right place. - - [ Simone Dalmasso ] - * [15750b] use the new uploader ui - * [a55b31] initial work on current permissions in perms widget - * [d0406e] add permissions for maps and documents - * [d68b9d] add actual users in the permission widget - * [e2c2a3] don't check permissions state in the new uploader - * [eb44ad] add fading to errors and correctly check them on item add - * [1a7da0] comment out not supported file types - * [512ce1] take out the console log - - [ capooti ] - * [bba77a] Refactored some code as there was an unassigned reference. - * [32c7ac] Updated the fixtures with the default_style fk value for the test layer. - - [ Ian Schneider ] - * [01422f] add reference api-doc for models, sphinx plugin - - [ capooti ] - * [befd6d] Moved the layer styles deletion logic from the view to the model's signals. - * [039da7] Adding a default title for style. - * [17f198] Added some test for the style stuff. - * [5e2d1d] Almost completed the "Edit style" tool to be used in the layer info page. Need to fix a pair of points with Bart before rasing a PR. - * [bc643d] Accessing the styler tool in the correct way. - - [ Simone Dalmasso ] - * [2b3cae] use "none" as keyword for the unbound permission widget - * [2701a9] initial work on document replace - * [faf849] document replace - * [fdaa33] use new permissions widget in documents - * [aba54a] use the response instead of the state of html in permissions widget - - [ Tom Kralidis ] - * [850a5e] editorial fixes (sort alphabetically, etc.) - - [ Simone Dalmasso ] - * [51619f] remove the word group from the permissions widget - * [5c1f4e] update the thumbnail on style change - - [ Ariel Núñ ] - * [f209eb] Upgraded gsconfig to 0.6.3, closes #761 - * [ea70bf] Do not load extjs in the upload form - * [e406e7] Import gsconfig errors classes at the top of the file, the seem to be ignored when accessed via dot notation - * [fa9fa7] Avoid multiple statements inside a big try/except block in upload - * [e9c0b2] Remove deprecated upload code - * [8c85c5] Remove Extjs from upload page - - [ Simone Dalmasso ] - * [66fda8] point the layer test to the new uploader url - * [b29569] verify that there are four different files before allowing upload - * [202cee] allow tif file - * [d317f4] also support the tiff extension - * [96a3e4] use the FileTypes to check files requirements - * [a4b6ff] show related documents in map and layer detail pages - * [14c951] fix document detail template and documents rating - * [0c132f] truncate before urlizing in info panel - * [b5fcb3] correct css order in map view, fix scalebar layout - * [2474ed] set style title to it's name if title is none - * [5f1e35] fix css issues in map composer - - [ Angelos Tzotsos ] - * [88d591] Updated spec packaging to 2.0beta1 - * [4ac946] More packages added to spec file - * [6bef3e] Created more packages to use in spec file - * [1f0eae] Added spec files for GeoExplorer, Avatar and gsconfig. Updated main spec file too. - * [57f302] Fixing previous commit - - [ Simone Dalmasso ] - * [4fc494] Remove the registration from the sign in drop down - * [32aadf] allow layer replace to respect the current permissions - * [a32bc6] include legend in map print - * [998dea] fix js tests and not test zip and csv. - * [7d4ecb] adjust the layer rating category in search tests - - [ Tom Kralidis ] - * [5deafd] pass just dict now that pycsw supports it - * [2389e1] bump OWSLib - * [106611] add labels for enums - - [ Paolo Pasquali ] - * [bd044e] center the nav-box - - [ Jeffrey Johnson ] - * [683f73] Add django-extensions - * [390f7d] Just ignore extraneous shapefile components for now - * [dfbcb7] Continued work to make rest based uploader work with new javascript. - * [4cf778] Make stop_django and stop_geoserver paver tasks - * [08f67b] Continued work on jquery uploader against rest api - * [8dc984] Handle for an empty jqXHR object when uploading - * [f8c13e] Fix typo - * [0b0187] Remove duplicate page in jquery uploader - - [ Simone Dalmasso ] - * [2ddd45] put layer, map and document count into topic category - * [160044] connect the signals for categories count - * [9716ab] update all tests to use real data instead of fixtures where needed - * [f2d225] add migrations for the topic categories count - - [ capooti ] - * [50d07f] For some reason I cannot figure out (maybe some sort of dependency) the avatar application it is not being synced when running syncd. I am adding back its initial migration, as it was some weeks ago, as in this way it is synced when running migrations. - - [ Simone Dalmasso ] - * [f75926] make use of require for search js - * [009285] remove no longer needed files - - [ Tom Kralidis ] - * [c4e2b0] remove SRID which is not needed in CSW context - * [3c1ef9] update travis-ci link [ci skip] - - [ capooti ] - * [dac27b] Added a migration to remove SRID from default value in the ResourceBase csw_wkt_geometry field. - * [3dac32] Removed the thumbnail field from the LayerForm. - - [ Jeffrey Johnson ] - * [a3f695] Dont fail miserably in cascading_delete if the workspsace is not found - * [08fc81] Add store_type property to layer model - - [ capooti ] - * [94c6c6] Removed the default_style and styles fields from the LayerForm - - [ Jeffrey Johnson ] - * [d1047d] Work from ROGUE on uploader - * [3c81ae] Further work on LayerInfo.js - - [ Simone Dalmasso ] - * [43e3f1] couple of small fixes - * [df22bc] add local geoserver if is missing - * [684198] check on the url existence rather than the entire source - - [ Jeffrey Johnson ] - * [867824] Further work in upload templates - - [ Simone Dalmasso ] - * [8f4921] handle any 403 by asking login and redirect back on success - - [ Jeffrey Johnson ] - * [b18e82] Make time page show up in 2 columns - * [120136] Continued work on multi-step uploads - * [b43805] Make next button in layer_upload_crs ajax - * [21244f] Enable csv/kml/zip in uploader - * [531601] Add doSrs and remove console.log statements - * [41b1cb] Further work on multi-step uploads - * [7b0ecc] Add srs.js - * [845369] Fix up error handling a bit - - [ Simone Dalmasso ] - * [ef1b86] add print service to embedded map - * [9ab2cd] move people page under the search engine. - - [ Tom Kralidis ] - * [27cac2] store originally uploaded XML instead of marshalling to ISO for uploaded metadata - - [ Ariel Nunez ] - * [0e5aa2] Removed try/except blocks from geonode_is_up - * [b81cb0] Fixed typo - - [ Simone Dalmasso ] - * [178201] update the integration test to reflect the new redirect to the login page - - [ Ariel Nunez ] - * [873d35] Added check in paver targetting python 2.7 for building geonode (this does not affect running) - - [ Jude Mwenda ] - * [0a4906] Metadata xml validation for date objects that are None - - [ Tom Kralidis ] - * [892bcb] bump pycsw - - [ Simone Dalmasso ] - * [50619d] middleware for printing private layers - * [7f34d4] handle any 403 by asking login and redirect back on success - * [550247] update the integration test to reflect the new redirect to the login page - - [ Jude Mwenda ] - * [a8d3e7] Metadata xml validation for date objects that are None - - [ Tom Kralidis ] - * [2d6779] bump pycsw - - [ Ariel Nunez ] - * [16affb] Switched to get_or_create on thumbnails - * [bcdcd9] Removing django-requests since it is not compatible with Django 1.5 - * [65665b] Added debug-false-only filter to mail_admins, see Django 1.4 release notes to learn why it is neede - * [8f018e] direct_to_template does not exist anymore, switched to TemplateView - * [b98d7e] Removing django-relationships since it is not compatible with 1.5. Also the author says it is an elaborate hack and should not be needed anymore: https://github.com/coleifer/django-relationships/pull/30 - * [9a3eb0] Used variables in urls where possible - - [ Jeffrey Johnson ] - * [062067] Fix url tag in layer_upload.html - - [ Simone Dalmasso ] - * [af0904] Add intégration test - * [4670b8] minor comments fix - - [ vagrant ] - - -- Ariel Nunez Fri, 14 Jun 2013 15:57:17 +0000 - -geonode (2.0.0+beta1) precise; urgency=high - - [ Simone Dalmasso ] - * [fa7d46] correctly check permissions on layers for new map with layers - * [94478b] use keyword slug instead of name in search - * [1eb916] don't look for layer uuid since search now returns the id - * [361b55] remove the paginate.js in layer detail - * [0e1625] permissions in new uploader - * [da46f2] correctly assign data type - - [ Matt Bertrand ] - * [704333] Make comment textarea wider - * [3046dd] Remove unnecessary class - * [fee410] Submit comments via ajax and reload comments section of page only - * [918abb] Use jquery & ajax to submit comment and reload comments div instead of entire page - * [c3bb71] Rename comment form and enclosing div id's - * [86f70a] Update comment width css to reflect modified element id's. - * [ad5d4d] Display number of ratings next to average rating for maps, layers, documents Created new base tagtemplate to return number of ratings for a given object & id - - [ Simone Dalmasso ] - * [3cc472] correctly assign the bbox_string used by download links - - [ Angelos Tzotsos ] - * [3c0061] updated geoserver specs to 2.3.0 - * [e5d90b] Added spec file for geonode-geoserver plugin - - [ Tom Kralidis ] - * [1410e6] strip whitespace from XML text() values - - [ Angelos Tzotsos ] - * [acb202] Added initial openSUSE rpm file for v2.0 - * [d474a0] Fix for #842 - - [ Simone Dalmasso ] - * [36e8e4] add bbox and crd parameters to wcs requests - * [1e6055] fix the height and width - * [8a5fe4] update new map to reflect the new bbox order - * [f2ea52] update map models to reflect the new bbox order - - [ Matthew Hanson ] - * [8c9b2a] fixed bad url name to registration - - [ Simone Dalmasso ] - * [61518c] remove unneeded pagination class - - [ Ariel Núñez ] - * [060d54] Updated changelog - * [db3d50] Reset version on master in preparation for beta - - -- Ariel Núñez Mon, 08 Apr 2013 14:02:56 +0000 - -geonode (2.0.0+alpha7) precise; urgency=high - - * [9248b1] Upgraded gsconfig.py - * [d122db] Upgraded django-geoexplorer - * [0c9675] Modify port for accessing GeoNode before building debian package. Fixes #808 - * [94edb1] Bumped to 2.0a7 - - -- Ariel Núñez Thu, 21 Mar 2013 13:29:43 +0000 - -geonode (2.0.0+alpha6) precise; urgency=high - - [ Simone Dalmasso ] - * [9d5a91] initial work on concrete resourcebase - * [02315a] make resource fk nullable - * [b83d1b] bump to base module in topicCategory fixtures - * [4447ee] move to new ContactRole model - * [095d9d] other minor adjustments - * [d5913f] put fixtures where should be - * [7af75c] fix more imports - * [616959] inherit from Resourcebase manager - * [33048d] some cleanup - * [af7807] move catalogue signals to ResourceBase - * [29407a] roll back to Role in people - * [f6685d] clean - - [ Ivan Willig ] - * [72047f] Handle if the download folder already exists - - [ Simone Dalmasso ] - * [290ded] get rid of the search pages - * [82f664] use search engine in home for layers - * [b66536] unify the rating functions in the proper template - * [32a3bc] add the rate.js file - - [ Ivan Willig ] - * [72e3b3] Use a try except ImportError to import the pushd function - * [3c0e24] Make the forward_mercator function's handling of edge cases better. - - [ Simone Dalmasso ] - * [188859] cleanup the paginate js - - [ Ariel Núñez ] - * [276537] Better error messages when catalogue does not create the record - * [eb7297] Getting rid of whitespace - * [682914] Disabling the catalogue until pycsw is upgraded - - [ Simone Dalmasso ] - * [324527] create the resource base in fixtures - * [508d4d] Make layer test pass - * [5e6bba] make map test to pass - * [a6495a] make documents test to pass - * [a0ecca] make search tests to pass - * [730325] reenable catalogue and make every test pass, fix search - * [77b672] move links to base, attache them to resourceBase and adjust the logic to make it work - * [444674] modify the default topic category behavior - * [1806c4] use the first in case none indicated - - [ Christian Spanring ] - * [5fd70f] updates minified base.css - * [1ca5a0] Adds CSS cursor pointer on maps and layer list pages - * [196d0f] Minor HTML tweak to eliminate heading indent on map_list page - * [9d40b1] Prevents orphaned maplayers from breaking the map detail views - - [ Simone Dalmasso ] - * [d06ddd] bump document detail to the new layout - * [a3d17f] general cleanup of unneeded code and some pyflakes clean - * [c4a377] add missed template - - [ Matthew Hanson ] - * [718ab5] fixed notification for updatelayers - - [ Christian Spanring ] - * [1bb139] Adds fallback to default auth backend in has_perm permission lookup function - * [5baf98] adds check for owner attr in obj in has_perm permission lookup - - [ Simone Dalmasso ] - * [71ced0] fix categories behavior - - [ Bart van den Eijnden ] - * [eb129c] make sure that Maps link in GeoExplorer works, thanks @cspanring for confirming that it works (closes #681) - - [ Simone Dalmasso ] - * [0d493b] add document remove and fix document permissions - * [3ed443] add document type and size check - * [db16a8] fix mb count in document upload - * [c07afa] remove unwanted ipdb - * [506ddd] fix profile detail to reflect ResourceBase - * [6bbd8b] make search add relevance query resourcebase - * [21273e] delegate to resource base post save the contactroles creation and management - * [4fe747] don't save again the layer after pc and ac change in metadata. - - [ Bart van den Eijnden ] - * [b166e0] restructure the GeoNode-GeoExplorer.js file so that it is easier to use an SDK application in GeoNode, a new mixin class was added in a separate file that is mixed in with GeoNode.Composer, an can be mixed in with any gxp.Viewer of an SDK application - * [066ad8] set rasterStyling to true on the styler plugin - * [b1a53a] add back accidentally removed code with https://github.com/GeoNode/geonode/pull/815 cc @jj0hns0n - * [392d57] fix issue reported by @simod I now get a Uncaught TypeError: Cannot read property 'Save' of undefined here both at layer/upload and document/upload pages. - - [ Jeffrey Johnson ] - * [8fc5c0] Switch travis to build master - - [ Bart van den Eijnden ] - * [a16f9b] add map_sdk.js and sdk_header.html template to make it easier to integrate OpenGeo Suite SDK apps in GeoNode - - [ Simone Dalmasso ] - * [c75422] get rid of ext in documents upload - * [ee2792] check if the file exists before putting the url (mostly for tests) - - [ Jeffrey Johnson ] - * [d70d4a] Fix _split_query import - * [972d33] Completely remove old layers/maps search implementation - * [1409df] Use consistent layer terminology in tests/smoke.py - * [1831e7] Make sure failing smoke tests fail the build - * [c08df9] Hook the javascript tests results up in jenkins - - [ Simone Dalmasso ] - * [026522] select2 icon was missing - - [ Ariel Núñez ] - * [5110a8] Updated changelog to alpha5 - - -- Ariel Núñez Wed, 20 Mar 2013 05:15:35 +0000 - -geonode (2.0.0+alpha5) precise; urgency=high - - [ Ariel Núñez ] - * [36d39b] Add back git-dch step in paver deb - * [3a0886] Do not add geoserver jars to the debian package, they are packaged elsewhere - * [888f83] Updated pavement script to properly place the deb artifact in launchpad - * [ab6b51] Reverted order of ppa name and version in paver deb task - * [4a9246] Fix version name and refactor ppa upload step - * [371b9d] Do not show vim when doing git-dch - - [ capooti ] - * [ae82ca] Deleting a layer, we need to delete all of its associations to maps containing it. It fixes #597. - - [ Ariel Núñez ] - * [fc33c0] Do not keep around old packages - * [5f282e] Fixed missing import - - [ Tom Kralidis ] - * [1584d6] bump OWSLib - - [ Ariel Núñez ] - * [9d0e7c] Switched to geoext by default - - [ Jeffrey Johnson ] - * [474be1] Change path of admin and devel workshops. - * [799945] Comment out jenkins code quality tools for now - * [3a553d] Add USER and PASSWORD to package/support/geonode.local_settings - - [ Ariel Núñez ] - * [5ecb99] USER and PASSWORD are optional for pycsw config - * [e3c958] Removed workaround for USER and PASSWORD from local_settings - * [54ea9e] Add more variations of ppa and key options to paver deb - * [149ad2] Automated package upload to ppa - * [4cb601] Requirements are already present on jenkins - * [631f33] Added -b option to git-dch to avoid problem in jenkins with version number - * [980f41] Avoid reading the branch name and hardcoded it to dev to make jenkins happy - - [ Jeffrey Johnson ] - * [ccfc05] Download the latest deb in setup_geonode_deb fabric task - * [bc1dbf] shell=True when trying to use wildcard in sudo fab command - * [fe8dc4] cd into the dir structure that wget creates before trying to run dpkg - * [7f04c3] Dont terminate instance after making AMI (this is temporary and dangerous longterm) - * [d39682] add deploy_geonode_snapshot_package to fabfile - * [46e3df] Just deploy a snapshot package for now, dont build the AMI - * [4436ac] some tweaks to docs script to publish workshops to main site - * [7e1896] Publish the PDF Docs too - - [ Bart van den Eijnden ] - * [63d908] fix 404s on the Amazon test instance, apparently these are all lowercase in GeoExplorer now, but did not run into this locally - - [ Ariel Núñez ] - * [7d4d06] Bumped geoexplorer version - * [c06cb1] Added dependency on python-django - * [fed618] Bump Django version to 1.4.3 - * [7964ec] Added gsconfig, owslib and pycsw dependencies - * [d69c79] geonode should depend only on python-pycsw, not on pycsw-cgi - * [293885] Using PPA dependencies in launchpad did not work, backing out gsconfig, owslib and pycsw debian packages from control file - - -- Ariel Núñez Sun, 13 Jan 2013 05:11:16 +0000 - -geonode (2.0.0+alpha0) precise; urgency=high - * Major source tree refactor - * Build refactor - * CSW refactor - - -- Ariel Nunez Wed, 8 Aug 2012 11:44:33 -0500 - -geonode (1.2+beta2) precise; urgency=high - * Added geonode-updateip command - * Added MapQuest and OSM tiles by default to whitelist - * geonode-updateip also adds the local host to the printing whitelist - * Fixed gsconfig 0.6 bug with vector datastores - * Reorganized LOGGING in settings - * Fixed bug related to default style not being present - * Upgraded gsconfig to version 0.6 - * Fixed missing links to local layers in map detail - * Map composer fixes - * More helpful importlayers - - -- Ariel Nunez Wed, 1 Aug 2012 11:44:33 -0500 - -geonode (1.2+beta1) precise; urgency=high - * Fixed a reverse url problem remove - * Added legend to layer detail page - * Added Bing AerialWithLabels layer as a background layer - * Fixed download links in GeoTiff layers - * Made keywords optional again - * Fixed keywords representation in layer detail page - * Fixed link to production docs - * Added post_save signals to create contact after user creation - * Fixed bug affecting map search - * Added CSW based search to map composer - * Added ability to delete layers with a shared style - - -- Ariel Nunez Sun, 24 Jun 2012 11:44:33 -0500 - -geonode (1.2+alpha2) precise; urgency=high - * Fixed a reverse url problem during upload - - -- Ariel Nunez Wed, 23 May 2012 11:44:33 -0500 - -geonode (1.2+alpha1) precise; urgency=high - * Updated locale files - * Switched GeoServer's default output strategy to FILE - * Renamed geonode_import to importlayers - * Better legends - * Updated gxp, geoext, openlayers and other submodules - * Added more background layers - * Added comments - * Added ratings - * Added plusone (Facebook and Google Plus) - * Upgraded to Django-1.4 - * Fixes for 12.04 compatibility (postgres9) - * Switched to lxml for parsing - * Added South for migrations - - -- Ariel Nunez Sun, 20 May 2012 18:44:33 -0500 - -geonode (1.1.1+final2) lucid; urgency=high - * Fixed bug in first time installs. - - -- Ariel Nunez Thu, 19 Mar 2012 18:44:33 -0500 - -geonode (1.1.1+final) lucid; urgency=high - * Fixed bug in deleting layers using a postgis backend. - - -- Ariel Nunez Thu, 12 Mar 2011 18:44:33 -0500 - -geonode (1.1+final) lucid; urgency=high - * Moved from SPEED to Partial Buffer in GeoServer's output strategy - * Proper upgrade support - * Fixed location of GeoServer's data dir - - -- Ariel Nunez Sun, 5 Feb 2011 18:44:33 -0500 - -geonode (1.1+RC2) lucid; urgency=high - * Fixed avatar links - * Added virtualenv upgrade to the installer to avoid bugs in Lucid - * Fixed admin media configuration - * Fixed login popup in embedded maps - * Removed the GetCapabilities calls in embedded maps - * Fixed install script, added pip upgrade - * Fixed path to geoserver data dir - - -- Ariel Nunez Fri, 2 Sept 2011 13:44:33 -0500 - -geonode (1.1-rc1) natty; urgency=high - * After creating installer - - -- Ariel Nunez Fri, 2 Sept 2011 13:44:33 -0500 - -geonode (1.1-beta4) natty; urgency=high - * Using latest master, after move to /geoserver - - -- Ariel Nunez Sun, 28 Aug 2011 17:44:33 -0500 - -geonode (1.1.beta+2) unstable; urgency=high - * Update to 1.1beta+2 and added gettext and postgres support - - -- Ariel Nunez Wed, 10 Aug 2011 17:44:33 -0500 - -geonode (1.0.final+1) unstable; urgency=high - * Add dependency on patch command - - -- David Winslow Tue, 14 Dec 2010 17:44:33 -0500 - -geonode (1.0+final) unstable; urgency=high - * Update to GeoNode 1.0-final - - -- David Winslow Tue, 14 Dec 2010 14:59:18 -0500 - -geonode (1.0+RC4) unstable; urgency=high - * Update to GeoNode 1.0RC4 - - * Force headless mode for tomcat service - - * Add dependency on libproj-dev - - -- David Winslow Mon, 06 Dec 2010 15:44:01 -0500 - -geonode (1.0+RC2) unstable; urgency=high - - * First complete release including pre and post install hooks. - - -- Aaron Greengrass Thu, 04 Nov 2010 14:49:39 -0700 diff --git a/package/debian/compat b/package/debian/compat deleted file mode 100644 index 1e8b3149621..00000000000 --- a/package/debian/compat +++ /dev/null @@ -1 +0,0 @@ -6 diff --git a/package/debian/config b/package/debian/config deleted file mode 100644 index 12e5f4a34da..00000000000 --- a/package/debian/config +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -. /usr/share/debconf/confmodule diff --git a/package/debian/control b/package/debian/control deleted file mode 100644 index 62d10fd8687..00000000000 --- a/package/debian/control +++ /dev/null @@ -1,21 +0,0 @@ -Source: geonode -Maintainer: Alessio Fabiani -Section: python -Priority: optional -Standards-Version: 3.9.1 -Homepage: http://geonode.org/ -Vcs-Browser: https://github.com/GeoNode/geonode -Vcs-git: git://github.com/GeoNode/geonode.git -Build-Depends: python-all-dev (>= 2.6.6-3), debhelper (>= 7.0.50~) - - -Package: geonode -Architecture: all -Depends: geoserver-geonode (= 2.14.0), apache2, libapache2-mod-wsgi, gdal-bin, libgeos-dev, gettext, postgresql-contrib, postgis, libpq-dev, zip, unzip, language-pack-en, libjpeg-dev, libpng-dev, libxslt1-dev, zlib1g-dev, libffi-dev, libssl-dev, python-setuptools (= 39.0.1-2), python-django, python-pip, python-celery, python-gdal, python-shapely (= 1.5.17-3), python-pyproj, libproj-dev, python-virtualenv, python-paver, python-elasticsearch (= 2.4.1-1), python-sqlalchemy, python-bs4, python-html5lib, python-webencodings, python-lxml, python-psycopg2, python-arcrest (= 10.3-1), python-dateutil, python-pil, python-httplib2, python-shapely (= 1.5.17-3), python-paver, python-gsconfig (= 1.0.10-1), python-gn-gsimporter (= 1.0.9-2), python-owslib (= 0.16.0-1), python-pycsw (= 2.2.0-1), python-decorator, python-timeout-decorator (= 0.4.0-1), python-six (= 1.10.0-1), python-django-allauth, python-django-bootstrap-form (= 3.4-1), python-django-forms-bootstrap (= 3.1.0-2), python-django-activity-stream (= 0.6.5-2), python-django-autocomplete-light (= 2.3.3-2), python-django-filters, python-django-ipware (= 2.1.0-1), python-django-multi-email-field (= 0.5.1-2), python-django-taggit, python-django-treebeard, python-django-mptt, python-django-guardian, python-django-tastypie, python-dj-database-url (= 0.4.2-1), python-pinax-notifications (= 4.1.0+geonode-1), python-backports.functools-lru-cache, python-boto3, python-constantly, python-django-geoexplorer (= 4.0.41-1), python-django-appconf, python-django-storages (= 1.6.5-2), python-django-floppyforms (= 1.7.0-2), python-django-invitations (= 1.9.2-2), python-django-bootstrap3-datetimepicker-2 (= 2.5.0+geonode-1), python-slugify, python-uwsgi (= 2.0.17-2), python-websocket-client (= 0.51.0-1), python-django-geonode-client, python-django-modeltranslation, python-geonode-user-messages (= 0.1.14-1), python-geonode-avatar (= 2.1.8-1), python-pinax-ratings (= 3.0.3), python-geonode-oauth-toolkit (= 1.1.2rc0-1), python-geonode-announcements (= 1.0.13+geonode-1), python-oauthlib (= 2.1.0-1), python-itypes, python-uritemplate, python-jinja2 (= 2.10-1), python-inflection, python-pygments, python-openid, python-configparser, python-sqlalchemy, python-psutil, python-django-cors-headers, python-requests, python-requests-toolbelt, python-django-downloadview, python-django-extra-views, python-django-polymorphic, python-django-basic-authentication-decorator (= 0.9-2), python-django-haystack, python-coverage, python-ply, python-pep8, python-pyshp, python-antlr, python-dicttoxml, python-datautil, python-enum34, python-geographiclib, python-geopy, python-glob2, python-gunicorn, python-hyperlink, python-simplegeneric, python-scandir, python-pyasn1, python-twisted-bin, python-pyasn1-modules, python-ipython-genutils, python-incremental, python-wcwidth, python-ptyprocess, python-zope.interface, python-serial, python-pathlib2, python-pam, python-traitlets, python-automat, python-service-identity, python-prompt-toolkit, python-pexpect, python-twisted-core, python-pickleshare, python-ipython, python-pycountry, python-lxml, python-jwcrypto, python-jdcal, python-maxminddb, python-parse-type, python-pillow, python-pluggy, python-hamcrest, python-openssl, python-pycodestyle, python-xmljson (= 0.1.9-2), python-user-agents (= 1.1.0-2), python-twisted, python-typing, python-py, python-tqdm, python-traitlets, python-mock, python-mako, python-invoke, python-memcached (= 1.59-2), python-yaml, python-dj-pagination (= 2.3.2-2), python-ua-parser (= 0.8.0-4), python-humanfriendly, ${misc:Depends}, ${python:Depends} -Recommends: python-gisdata -Description: Allows the creation, sharing, and collaborative use of geospatial data. - At its core, the GeoNode has a stack based on GeoServer, pycsw, - Django, and GeoExt that provides a platform for sophisticated - web browser spatial visualization and analysis. Atop this stack, - the project has built a map composer and viewer, tools for - analysis, and reporting tools. diff --git a/package/debian/copyright b/package/debian/copyright deleted file mode 100644 index f1cfbde58c1..00000000000 --- a/package/debian/copyright +++ /dev/null @@ -1,37 +0,0 @@ -This work was packaged for Debian by: - - Aaron Greengrass on Thu, 04 Nov 2010 14:49:39 -0700 - -It was downloaded from: - - http://geonode.org/ - -Copyright: - - Copyright (C) 2010 David Winslow - Copyright (C) 2016 OSGeo - -License: - - 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 package 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 . - -On Debian systems, the complete text of the GNU General -Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". - -The Debian packaging is: - - Copyright (C) 2010 Aaron Greengrass - -and is licensed under the GPL version 3, see above. - diff --git a/package/debian/docs b/package/debian/docs deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/package/debian/postinst b/package/debian/postinst deleted file mode 100644 index 382131b0242..00000000000 --- a/package/debian/postinst +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# postinst script for geonode -# -# see: dh_installdeb(1) - -set -e - -# summary of how this script can be called: -# * `configure' -# * `abort-upgrade' -# * `abort-remove' `in-favour' -# -# * `abort-remove' -# * `abort-deconfigure' `in-favour' -# `removing' -# -# for details, see http://www.debian.org/doc/debian-policy/ or -# the debian-policy package - - -case "$1" in - configure) - if [ "$2" = "1.1+RC2" ] - then - # Migrate old varchar fields to TEXT - su - postgres -c 'psql geonode' <&2 - exit 1 - ;; -esac - -exit 0 diff --git a/package/debian/postrm b/package/debian/postrm deleted file mode 100644 index 86f5209225c..00000000000 --- a/package/debian/postrm +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh -# post script for geonode -# -# see: dh_installdeb(1) - -set -e - -case "$1" in - remove|deconfigure) - a2dissite geonode.conf - - rm -rf /var/lib/geonode - rm -rf /var/www/geonode - - rm -rf /usr/share/geonode/role.sql - rm -rf /usr/share/geonode/admin.json - rm -rf /usr/share/geonode/patch* - rm -rf /usr/bin/geonode - rm -rf /usr/share/geoserver - - a2ensite 000-default.conf - - invoke-rc.d apache2 start - invoke-rc.d tomcat8 start - ;; - - upgrade) - rm -rf /var/lib/geonode/src - rm -rf /var/lib/geonode/build - rm -rf /var/lib/geonode/lib - ;; - purge) - if su - postgres -c 'psql -l | grep -q geonode' - then - su - postgres -c 'dropdb geonode_data' - su - postgres -c 'dropdb geonode' - su - postgres -c 'dropuser geonode' - fi - ;; - failed-upgrade) - ;; - - *) - echo "prerm called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -#DEBHELPER# - -exit 0 diff --git a/package/debian/preinst b/package/debian/preinst deleted file mode 100644 index 1c8178f4d30..00000000000 --- a/package/debian/preinst +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -# preinst script for geonode -# -# see: dh_installdeb(1) - -set -e - -# summary of how this script can be called: -# * `configure' -# * `abort-upgrade' -# * `abort-remove' `in-favour' -# -# * `abort-remove' -# * `abort-deconfigure' `in-favour' -# `removing' -# -# for details, see http://www.debian.org/doc/debian-policy/ or -# the debian-policy package - - -case "$1" in - upgrade) - case "$2" in - 1.1+RC2) - rsync -ar /var/lib/tomcat6/webapps/geoserver/data/ /var/lib/geoserver/geonode-data/ - find /var/lib/geoserver/geonode-data/workspaces/ -name '*.xml' | \ - xargs sed -i '\!!,\!<\/keywords>! s!\s*!!' - sed -i '/GEOSERVER_DATA_DIR/,+1 s!param-value>.*/var/lib/geoserver/geonode-data/&2 - exit 1 - ;; -esac - -exit 0 diff --git a/package/debian/prerm b/package/debian/prerm deleted file mode 100644 index 315b4d57086..00000000000 --- a/package/debian/prerm +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/sh -# prerm script for geonode -# -# see: dh_installdeb(1) - -set -e - -# summary of how this script can be called: -# * `remove' -# * `upgrade' -# * `failed-upgrade' -# * `remove' `in-favour' -# * `deconfigure' `in-favour' -# `removing' -# -# for details, see http://www.debian.org/doc/debian-policy/ or -# the debian-policy package - - -case "$1" in - remove|upgrade|deconfigure) - invoke-rc.d apache2 stop - invoke-rc.d tomcat8 stop - ;; - - failed-upgrade) - ;; - - *) - echo "prerm called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -# dh_installdeb will replace this with shell code automatically -# generated by other debhelper scripts. - -#DEBHELPER# - -exit 0 diff --git a/package/debian/rules b/package/debian/rules deleted file mode 100755 index fcefb532407..00000000000 --- a/package/debian/rules +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- -# Sample debian/rules that uses debhelper. -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -%: - dh $@ -install: - dh_testdir - dh_testroot - dh_prep - dh_installdirs - # Create a dir called geonode and unzip the tarball there - mkdir -p $(CURDIR)/geonode - tar zxvf $(CURDIR)/GeoNode*.tar.gz -C $(CURDIR)/geonode --strip 1 - # Call geonode's install.sh with the pre flag. - $(CURDIR)/geonode/install.sh -s pre $(CURDIR)/debian/support/config-pre.sh - # Copying installer and configuration to be used by post installer - cp $(CURDIR)/geonode/install.sh $(CURDIR)/debian/geonode/usr/share/geonode - cp $(CURDIR)/debian/support/config-post.sh $(CURDIR)/debian/geonode/usr/share/geonode - rm -rf $(CURDIR)/geonode -binary-indep: build install - -binary-arch: build install - dh_testdir - dh_testroot -# dh_installdebconf - dh_installdocs -# dh_installmenu -# dh_link -# dh_strip -# dh_compress - dh_fixperms - dh_installdeb - dh_gencontrol - dh_md5sums - dh_builddeb - -binary: binary-indep binary-arch -# .PHONY: build clean binary-indep binary-arch binary install - diff --git a/package/debian/source/format b/package/debian/source/format deleted file mode 100644 index 89ae9db8f88..00000000000 --- a/package/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/package/debian/support/config-post.sh b/package/debian/support/config-post.sh deleted file mode 100644 index 41c99adb528..00000000000 --- a/package/debian/support/config-post.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# Location of the expanded GeoNode tarball -INSTALL_DIR=. -# Location of the target filesystem, it may be blank -# or something like $(CURDIR)/debian/geonode/ -TARGET_ROOT='' -# Tomcat webapps directory -TOMCAT_WEBAPPS=$TARGET_ROOT/var/lib/tomcat8/webapps -# Geoserver data dir, it will survive removals and upgrades -GEOSERVER_DATA_DIR=$TARGET_ROOT/var/lib/geoserver/geonode-data -# Place where GeoNode media is going to be served -GEONODE_WWW=$TARGET_ROOT/var/www/geonode -# Apache sites directory -APACHE_SITES=$TARGET_ROOT/etc/apache2/sites-available -# Place where the GeoNode virtualenv would be installed -GEONODE_LIB=$TARGET_ROOT/var/lib/geonode -# Path to preferred location of binaries (might be /usr/sbin for CentOS) -GEONODE_BIN=$TARGET_ROOT/usr/sbin/ -# Path to place miscelaneous patches and scripts used during the install -GEONODE_SHARE=$TARGET_ROOT/usr/share/geonode -# Path to GeoNode configuration and customization -GEONODE_ETC=$TARGET_ROOT/etc/geonode -# Path to GeoNode logging folder -GEONODE_LOG=$TARGET_ROOT/var/log/geonode -# OS preferred way of starting or stopping services -# for example 'service httpd' or '/etc/init.d/apache2' -APACHE_SERVICE="invoke-rc.d apache2" -# sama sama -TOMCAT_SERVICE="invoke-rc.d tomcat8" - -# For Ubuntu 12.04 (with PostGIS 1.5) -if [ -d "/usr/share/postgresql/9.1/contrib/postgis-1.5" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql/9.1/contrib/postgis-1.5 - POSTGIS_SQL=postgis.sql - GEOGRAPHY=1 -else - GEOGRAPHY=0 -fi - -# For Ubuntu 14.04 (with PostGIS 2.1) -if [ -d "/usr/share/postgresql/9.3/contrib/postgis-2.1" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql/9.3/contrib/postgis-2.1 - POSTGIS_SQL=postgis.sql - GEOGRAPHY=1 -else - GEOGRAPHY=0 -fi - -# For Ubuntu 16.04 (with PostGIS 2.2) -if [ -d "/usr/share/postgresql/9.5/contrib/postgis-2.2" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql/9.5/contrib/postgis-2.2 - POSTGIS_SQL=postgis.sql - GEOGRAPHY=1 -else - GEOGRAPHY=0 -fi diff --git a/package/debian/support/config-pre.sh b/package/debian/support/config-pre.sh deleted file mode 100644 index ed76b11a606..00000000000 --- a/package/debian/support/config-pre.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# Location of the expanded GeoNode tarball -INSTALL_DIR=geonode -# Location of the target filesystem, it may be blank -# or something like $(CURDIR)/debian/geonode/ -TARGET_ROOT=debian/geonode -# Tomcat webapps directory -TOMCAT_WEBAPPS=$TARGET_ROOT/var/lib/tomcat8/webapps -# Geoserver data dir, it will survive removals and upgrades -GEOSERVER_DATA_DIR=$TARGET_ROOT/var/lib/geoserver/geonode-data -# Place where GeoNode media is going to be served -GEONODE_WWW=$TARGET_ROOT/var/www/geonode -# Apache sites directory -APACHE_SITES=$TARGET_ROOT/etc/apache2/sites-available -# Place where the GeoNode virtualenv would be installed -GEONODE_LIB=$TARGET_ROOT/var/lib/geonode -# Path to preferred location of binaries (might be /usr/sbin for CentOS) -GEONODE_BIN=$TARGET_ROOT/usr/sbin/ -# Path to place miscelaneous patches and scripts used during the install -GEONODE_SHARE=$TARGET_ROOT/usr/share/geonode -# Path to GeoNode configuration and customization -GEONODE_ETC=$TARGET_ROOT/etc/geonode -# Path to GeoNode logging folder -GEONODE_LOG=$TARGET_ROOT/var/log/geonode -# OS preferred way of starting or stopping services -# for example 'service httpd' or '/etc/init.d/apache2' -APACHE_SERVICE="invoke-rc.d apache2" -# sama sama -TOMCAT_SERVICE="invoke-rc.d tomcat8" - -# For Ubuntu 12.04 (with PostGIS 1.5) -if [ -d "/usr/share/postgresql/9.1/contrib/postgis-1.5" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql/9.1/contrib/postgis-1.5 - POSTGIS_SQL=postgis.sql - GEOGRAPHY=1 -else - GEOGRAPHY=0 -fi - -# For Ubuntu 14.04 (with PostGIS 2.1) -if [ -d "/usr/share/postgresql/9.3/contrib/postgis-2.1" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql/9.3/contrib/postgis-2.1 - POSTGIS_SQL=postgis.sql - GEOGRAPHY=1 -else - GEOGRAPHY=0 -fi diff --git a/package/geoserver/README b/package/geoserver/README deleted file mode 100644 index 79810dae29e..00000000000 --- a/package/geoserver/README +++ /dev/null @@ -1,7 +0,0 @@ -To build the debian package, you need to do the following. - -1. Download geoserver.war from the build server -2. Rename the war to zip and unzip in target/geoserver -3. Update debian/changelog with the new version -4. Run debuild -S -k -5. dput ppa:geonode/testing ../geoserver-geonode*.sources diff --git a/package/geoserver/build_geonode-geoserver-ext-deb.sh b/package/geoserver/build_geonode-geoserver-ext-deb.sh deleted file mode 100755 index 3dcd657dcff..00000000000 --- a/package/geoserver/build_geonode-geoserver-ext-deb.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -set -e - -DL_ROOT=/var/www/geoserver - -# Make sure last job cleaned up -if [ -d ./tmp ]; then - rm -rf ./tmp -fi - -# Checkout exts from server -GEOSERVER_EXT_GIT=git://github.com/GeoNode/geoserver-geonode-ext.git -git clone $GEOSERVER_EXT_GIT tmp - -cp -r debian tmp -pushd tmp -git checkout $GIT_BRANCH - -# Replace GeoNode port for production. -sed -i 's/localhost:8000/127.0.0.1/g' \ - src/main/java/org/geonode/security/GeoNodeSecurityProvider.java - -git config user.email "mweisman@opengeo.org" -git config user.name "Michael Weisman" - -GIT_REV=$(git log -1 --pretty=format:%h) - -DEB_VERSION=2.0+$(date +"%Y%m%d%H%M") - -mvn clean install war:war - -# Build for launchpad -#git-dch --spawn-editor=snapshot --new-version=$DEB_VERSION --git-author --id-length=6 --ignore-branch --auto --release -#sed -i 's/urgency=low/urgency=high/g' \ -# debian/changelog - -#debuild -S -#dput ppa:geonode/$PPA ../geoserver-geonode_${DEB_VERSION}_source.changes -#rm ../geoserver-geonode* - -# Re-build local debs -#debuild - -# Copy .debs, .jar, and .war into place on the server -if [ -d $DL_ROOT/$GIT_REV ]; then - rm -rf $DL_ROOT/$GIT_REV -fi - -mkdir $DL_ROOT/$GIT_REV -#cp ../*.deb $DL_ROOT/$GIT_REV/. -cp target/geoserver.war $DL_ROOT/$GIT_REV/. -cp target/geonode-geoserver-ext-*-geoserver-plugin.zip $DL_ROOT/$GIT_REV/. -cp target/*data.zip $DL_ROOT/$GIT_REV/data.zip - -# Remove all but last 4 builds to stop disk from filling up -(ls -t|tail -n 3)|sort|uniq -u | xargs rm -rf - -# Cleanup -rm -rf $DL_ROOT/latest -ln -sf $DL_ROOT/$GIT_REV $DL_ROOT/latest -#rm ../geoserver-geonode* - -popd -rm -rf tmp diff --git a/package/geoserver/build_geonode-geoserver-ext-rpm.sh b/package/geoserver/build_geonode-geoserver-ext-rpm.sh deleted file mode 100755 index dd086b347e8..00000000000 --- a/package/geoserver/build_geonode-geoserver-ext-rpm.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -set -e - -export GEONODE_EXT_ROOT=$PWD/tmp -DL_ROOT=/var/www/geoserver - -# Make sure last job cleaned up -if [ -d ./tmp ]; then - rm -rf ./tmp -fi - -# Checkout exts from server -GEOSERVER_EXT_GIT=git://github.com/GeoNode/geoserver-geonode-ext.git -git clone $GEOSERVER_EXT_GIT tmp - -cp -r rpm tmp -pushd tmp -GIT_REV=$(git log -1 --pretty=format:%h) - -# Build RPM -rpmbuild --define "_topdir ${PWD}/rpm" -bb rpm/SPECS/geoserver.spec - -# Copy .rpms into place on the server -scp -P 7777 -i ~/.ssh/jenkins_key.pem ../*.rpm jenkins@build.geonode.org:$DL_ROOT/$GIT_REV - -# Cleanup -rm ../*.rpm - -popd -rm -rf tmp diff --git a/package/geoserver/debian/changelog b/package/geoserver/debian/changelog deleted file mode 100644 index 6f6f6549575..00000000000 --- a/package/geoserver/debian/changelog +++ /dev/null @@ -1,89 +0,0 @@ -geoserver-geonode (2.14.0) bionic; urgency=high - - * Updated GeoServer 2.14.x packages on testing - - -- Alessio Fabiani Mon, 03 Sep 2018 10:48:48 +0200 - -geoserver-geonode (2.13.2) bionic; urgency=high - - * Updated GeoServer 2.13.x packages on testing - - -- Alessio Fabiani Mon, 20 Aug 2018 12:29:29 +0200 - -geoserver-geonode (2.12.2-4) xenial; urgency=high - - * Fixed Tomcat 8 dependencies on Catalina properties - - -- Alessio Fabiani Fri, 30 Mar 2018 15:50:42 +0200 - -geoserver-geonode (2.12.2-3) xenial; urgency=high - - * Updated GeoServer 2.12.x packages on testing - - -- Alessio Fabiani Fri, 30 Mar 2018 15:07:42 +0200 - -geoserver-geonode (2.12.2-2) xenial; urgency=high - - * Updated GeoServer 2.12.x packages on testing - - -- Alessio Fabiani Fri, 30 Mar 2018 13:10:42 +0200 - -geoserver-geonode (2.12.1) xenial; urgency=high - - * Updated GeoServer 2.12.x packages on testing - - -- Alessio Fabiani Mon, 15 Jan 2018 10:26:16 +0200 - -geoserver-geonode (2.12.0) xenial; urgency=high - - * Updated to GeoServer 2.12.x - - -- Alessio Fabiani Mon, 15 Jan 2018 10:26:16 +0200 - -geoserver-geonode (2.10.0) xenial; urgency=high - - * Updated to GeoServer 2.10.x - * Added backup and restore - * Added oauth2 plugin - - -- Alessio Fabiani Wed, 06 Sep 2017 12:07:41 +0200 - -geoserver-geonode (2.9.0) xenial; urgency=high - - * Added backup and restore - * Added oauth2 plugin - - -- Simone Dalmasso Mon, 16 Jan 2017 10:00:41 -0500 - -geoserver-geonode (2.7.4-2) trusty; urgency=high - - * GeoServer needs to be unzipped in target/geoserver - * Added README to create this package - - -- Ariel Nunez Thu, 19 Nov 2015 11:59:41 -0500 - - -geoserver-geonode (2.7.4-1) trusty; urgency=high - - * Fixed packaging problem that affected the build - - -- Ariel Nunez Thu, 19 Nov 2015 11:59:41 -0500 - - -geoserver-geonode (2.7.4) trusty; urgency=medium - - * GeoServer 2.7.4 by Alessio Fabiani - - -- Ariel Nunez Thu, 19 Nov 2015 11:53:41 -0500 - -geoserver-geonode (2.4+beta25) precise; urgency=low - - * Updates for Geonode 2.0 - - -- Michael Weisman Mon, 17 Dec 2012 11:23:45 +0800 - -geoserver-geonode (0.3) precise; urgency=low - - * Initial release. (Closes: #1) - - -- Ariel Nunez Sun, 11 Nov 2012 03:18:45 +0000 diff --git a/package/geoserver/debian/compat b/package/geoserver/debian/compat deleted file mode 100644 index 7f8f011eb73..00000000000 --- a/package/geoserver/debian/compat +++ /dev/null @@ -1 +0,0 @@ -7 diff --git a/package/geoserver/debian/control b/package/geoserver/debian/control deleted file mode 100755 index d931a11609d..00000000000 --- a/package/geoserver/debian/control +++ /dev/null @@ -1,20 +0,0 @@ -Source: geoserver-geonode -Section: science -Priority: extra -Build-Depends: debhelper (>= 7.0.50~), unzip -Maintainer: Alessio Fabiani -Homepage: http://geonode.org -Vcs-Browser: https://github.com/GeoNode/geonode -Vcs-git: git://github.com/geonode/geonode.git - -Package: geoserver-geonode -Architecture: all -Depends: tomcat8, tomcat8-admin -Description: High performance, standards-compliant map and geospatial data server. - GeoServer is an open source software server written in Java that allows users to share - and edit geospatial data. Contains GeoNode extensions. - -Package: geoserver-geonode-suite -Architecture: all -Depends: opengeo-geoserver -Description: GeoNode extensions for The OpenGeo Suite. diff --git a/package/geoserver/debian/copyright b/package/geoserver/debian/copyright deleted file mode 100644 index f6056d87c6f..00000000000 --- a/package/geoserver/debian/copyright +++ /dev/null @@ -1,351 +0,0 @@ -GeoServer is distributed under the GNU General Public License Version 2.0 license: - - GeoServer, open geospatial information server - Copyright (C) 2014 - 2016 Open Source Geospatial Foundation - Copyright (C) 2001 - 2014 The Open Planning Project dba OpenPlans - http://openplans.org - - 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 2 of the License, or - (at your option) any later version (collectively, "GPL"). - - As an exception to the terms of the GPL, you may copy, modify, - propagate, and distribute a work formed by combining GeoServer with the - Eclipse Libraries, or a work derivative of such a combination, even if - such copying, modification, propagation, or distribution would otherwise - violate the terms of the GPL. Nothing in this exception exempts you from - complying with the GPL in all respects for all of the code used other - than the Eclipse Libraries. You may include this exception and its grant - of permissions when you distribute GeoServer. Inclusion of this notice - with such a distribution constitutes a grant of such permissions. If - you do not wish to grant these permissions, remove this paragraph from - your distribution. "GeoServer" means the GeoServer software licensed - under version 2 or any later version of the GPL, or a work based on such - software and licensed under the GPL. "Eclipse Libraries" means Eclipse - Modeling Framework Project and XML Schema Definition software - distributed by the Eclipse Foundation and licensed under the Eclipse - Public License Version 1.0 ("EPL"), or a work based on such software and - licensed under the EPL. - - 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, write to the Free Software - Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA - -For latest contact information of The Open Planning Project see the -website at http://topp.openplans.org. Current email is info@openplans.org -and address is 349 W. 12th Street, New York, NY 10014. - -The full GPL license is available in this directory at GPL.txt - -************************************************************************* -Additional Libraries and Code used -************************************************************************* -GeoServer uses several additional libraries and pieces of code. We are -including the appropriate notices in this file. We'd like to thank all -the creators of the libraries we rely on, GeoServer would certainly not -be possible without them. There are also several LGPL libraries that do -not require us to cite them, but we'd like to thank GeoTools - -http://geotools.org, JTS - http://www.vividsolutions.com/jts/jtshome.htm - WKB4J http://wkb4j.sourceforge.net iText - http://www.lowagie.com/iText/ -and J. David Eisenberg's PNG encoder http://www.catcode.com/pngencoder/ - -GeoServer also thanks Anthony Dekker for the NeuQuant Neural-Net Quantization -Algorithm. The copyright notice is intact in the source code and also here: - -/* NeuQuant Neural-Net Quantization Algorithm - * ------------------------------------------ - * - * Copyright (c) 1994 Anthony Dekker - * - * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. - * See "Kohonen neural networks for optimal colour quantization" - * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. - * for a discussion of the algorithm. - * - * Any party obtaining a copy of these files from the author, directly or - * indirectly, is granted, free of charge, a full and unrestricted irrevocable, - * world-wide, paid up, royalty-free, nonexclusive right and license to deal - * in this software and documentation files (the "Software"), including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons who receive - * copies from any such party to do so, with the only requirement being - * that this copyright notice remain intact. - */ - - -The GeoServer Project also thank J. M. G. Elliot for his improvements on -Jef Poskanzer's GifEncoder. Notice is included below on his Elliot's -release to public domain and Poskanzer's original notice (which is new -BSD). Source code is included in GeoServer source, with modifications done -by David Blasby for TOPP. - ------- -Since Gif89Encoder includes significant sections of code from Jef Poskanzer's -GifEncoder.java, I'm including its notice in this distribution as requested (appended -below). - -As for my part of the code, I hereby release it, on a strictly "as is" basis, -to the public domain. - -J. M. G. Elliott -15-Jul-2000 - ---------------------- from Jef Poskanzer's GifEncoder.java --------------------- - -// GifEncoder - write out an image as a GIF -// -// Transparency handling and variable bit size courtesy of Jack Palevich. -// -// Copyright (C) 1996 by Jef Poskanzer . All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// 1. Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -// SUCH DAMAGE. -// -// Visit the ACME Labs Java page for up-to-date versions of this and other -// fine Java utilities: http://www.acme.com/java/ ------- - - -JAI Image-io jars from Sun are also included. These are released under a -BSD license (new). Notice is below: - -------- -Initial sources - -Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -- Redistribution of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -- Redistribution in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - -Neither the name of Sun Microsystems, Inc. or the names of -contributors may be used to endorse or promote products derived -from this software without specific prior written permission. - -This software is provided "AS IS," without a warranty of any -kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND -WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY -EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL -NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF -USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS -DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR -ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, -CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND -REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR -INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - -You acknowledge that this software is not designed or intended for -use in the design, construction, operation or maintenance of any -nuclear facility. -------- - -GeoServer also includes binaries and from Jetty, the standard version can be -found at http://jetty.mortbay.org, released under an OSI-approved artistic -license. We include the license completely, as some versions will be -distributed without full source. - ------- -Jetty License -$Revision: 3.7 $ -Preamble: - -The intent of this document is to state the conditions under which the Jetty Package may be copied, such that the Copyright Holder maintains some semblance of control over the development of the package, while giving the users of the package the right to use, distribute and make reasonable modifications to the Package in accordance with the goals and ideals of the Open Source concept as described at http://www.opensource.org. - -It is the intent of this license to allow commercial usage of the Jetty package, so long as the source code is distributed or suitable visible credit given or other arrangements made with the copyright holders. - -Definitions: - - * "Jetty" refers to the collection of Java classes that are distributed as a HTTP server with servlet capabilities and associated utilities. - - * "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. - - * "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder. - - * "Copyright Holder" is whoever is named in the copyright or copyrights for the package. - Mort Bay Consulting Pty. Ltd. (Australia) is the "Copyright Holder" for the Jetty package. - - * "You" is you, if you're thinking about copying or distributing this Package. - - * "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) - - * "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. - -0. The Jetty Package is Copyright (c) Mort Bay Consulting Pty. Ltd. (Australia) and others. Individual files in this package may contain additional copyright notices. The javax.servlet packages are copyright Sun Microsystems Inc. - -1. The Standard Version of the Jetty package is available from http://jetty.mortbay.org. - -2. You may make and distribute verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you include this license and all of the original copyright notices and associated disclaimers. - -3. You may make and distribute verbatim copies of the compiled form of the Standard Version of this Package without restriction, provided that you include this license. - -4. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. - -5. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: - - a) Place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as ftp.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. - - b) Use the modified Package only within your corporation or organization. - - c) Rename any non-standard classes so the names do not conflict with standard classes, which must also be provided, and provide a separate manual page for each non-standard class that clearly documents how it differs from the Standard Version. - - d) Make other arrangements with the Copyright Holder. - -6. You may distribute modifications or subsets of this Package in source code or compiled form, provided that you do at least ONE of the following: - - a) Distribute this license and all original copyright messages, together with instructions (in the about dialog, manual page or equivalent) on where to get the complete Standard Version. - - b) Accompany the distribution with the machine-readable source of the Package with your modifications. The modified package must include this license and all of the original copyright notices and associated disclaimers, together with instructions on where to get the complete Standard Version. - - c) Make other arrangements with the Copyright Holder. - -7. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you meet the other distribution requirements of this license. - -8. Input to or the output produced from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this Package. - -9. Any program subroutines supplied by you and linked into this Package shall not be considered part of this Package. - -10. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. - -11. This license may change with each release of a Standard Version of the Package. You may choose to use the license associated with version you are using or the license of the latest Standard Version. - -12. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. - -13. If any superior law implies a warranty, the sole remedy under such shall be , at the Copyright Holders option either a) return of any price paid or b) use or reasonable endeavours to repair or replace the software. - -14. This license shall be read under the laws of Australia. -------- - - -GeoServer includes a few snippets from the Prototype library (www.prototypejs.org), -under a MIT license: - -------- -Copyright (c) 2005-2007 Sam Stephenson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -------- - -GeoServer uses a number of libraries licensed under the Apache License, -Version 2.0. These include Spring - http://www.springsource.org/, a number of Apache commons libraries - http://jakarta.apache.org/commons/ -whose jars we distribute and include in our source tree under lib/. Also -included as libraries are log4 http://logging.apache.org/log4j/docs/index.htmlj, -batik http://xmlgraphics.apache.org/batik/, xerces http://xerces.apache.org/xerces-j/ -and xalan http://xml.apache.org/xalan-j/. Note there is some disagreement as to -whether GPL and Apache 2.0 are compatible see -http://www.apache.org/licenses/GPL-compatibility.html for more information. We -hope that something will work out, as GeoServer would not be possible without -apache libraries. Notice for apache license is included below: -------- -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and - - 2. You must cause any modified files to carry prominent notices stating that You changed the files; and - - 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS -------- - -GeoServer is build using a number of eclipse libraries including emf and xsd made available under the Eclipse Public License. - -The notice for EPL license is included below: - - Copyright (c) 2002-2006 IBM Corporation and others. - All rights reserved. This program and the accompanying materials - are made available under the terms of the Eclipse Public License v1.0 - which accompanies this distribution, and is available at - http://www.eclipse.org/legal/epl-v10.html diff --git a/package/geoserver/debian/geoserver-geonode-suite.postinst b/package/geoserver/debian/geoserver-geonode-suite.postinst deleted file mode 100755 index 1a6c9332cbb..00000000000 --- a/package/geoserver/debian/geoserver-geonode-suite.postinst +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -# start tomcat after installing geoserver -service tomcat8 restart diff --git a/package/geoserver/debian/geoserver-geonode.postinst b/package/geoserver/debian/geoserver-geonode.postinst deleted file mode 100755 index aaa428a164a..00000000000 --- a/package/geoserver/debian/geoserver-geonode.postinst +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -# Back up tomcat8 config file before changing it -cp /etc/default/tomcat8 /etc/default/tomcat8.orig - -#GeoServer needs more ram than the default for tomcat. -JVM_OPTS='JAVA_OPTS="-Djava.awt.headless=true -Xms2048m -Xmx2048m -XX:+UseParallelOldGC -XX:+UseParallelGC -XX:NewRatio=2 -XX:+AggressiveOpts -Xrs -XX:PerfDataSamplingInterval=500 -XX:MaxPermSize=128m"' - -# Append a line with the new jvm configuration -if [ "$(grep ^GEOSERVER /etc/default/tomcat8)" == "" ]; then - echo '# GEOSERVER additions' >> /etc/default/tomcat8 - echo 'JAVA_HOME=/usr/' >> /etc/default/tomcat8 - echo $JVM_OPTS >> /etc/default/tomcat8 - sed -i -e 's/xom-\*\.jar/xom-\*\.jar,bcprov\*\.jar/g' /var/lib/tomcat8/conf/catalina.properties - mv /etc/tomcat8/tomcat-users.xml /etc/tomcat8/tomcat-users.xml.org - echo '> /etc/tomcat8/tomcat-users.xml - echo ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' >> /etc/tomcat8/tomcat-users.xml - echo ' xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"' >> /etc/tomcat8/tomcat-users.xml - echo ' version="1.0">' >> /etc/tomcat8/tomcat-users.xml - echo ' ' >> /etc/tomcat8/tomcat-users.xml - echo ' ' >> /etc/tomcat8/tomcat-users.xml - echo ' ' >> /etc/tomcat8/tomcat-users.xml - echo '' >> /etc/tomcat8/tomcat-users.xml -fi - -# Fix permissions on deployed jar -chown -R tomcat8:tomcat8 /usr/share/geoserver/ - -# start tomcat after installing geoserver -service tomcat8 restart diff --git a/package/geoserver/debian/mvn_settings.xml b/package/geoserver/debian/mvn_settings.xml deleted file mode 100644 index fdfc2d27a8a..00000000000 --- a/package/geoserver/debian/mvn_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - $HOME/.mvn/ - diff --git a/package/geoserver/debian/preinst b/package/geoserver/debian/preinst deleted file mode 100755 index e69de29bb2d..00000000000 diff --git a/package/geoserver/debian/rules b/package/geoserver/debian/rules deleted file mode 100755 index 339893488b0..00000000000 --- a/package/geoserver/debian/rules +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- -# Sample debian/rules that uses debhelper. -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -%: - dh $@ - -install: - dh_testdir - dh_testroot - dh_prep - dh_installdirs - # copy geoserver in /usr/share/geoserver - mkdir -p $(CURDIR)/debian/geoserver-geonode/usr/share/geoserver/ - cp -r $(CURDIR)/target/geoserver/* $(CURDIR)/debian/geoserver-geonode/usr/share/geoserver/. - # configure geoserver in tomcat - mkdir -p $(CURDIR)/debian/geoserver-geonode/etc/tomcat8/Catalina/localhost - echo '' >> $(CURDIR)/debian/geoserver-geonode/etc/tomcat8/Catalina/localhost/geoserver.xml - -binary-indep: install - -binary-arch: install - dh_testdir - dh_testroot - dh_installdocs - dh_fixperms - dh_installdeb - dh_gencontrol - dh_md5sums - dh_builddeb - -binary: binary-indep binary-arch diff --git a/package/geoserver/rpm/SPECS/geoserver.spec b/package/geoserver/rpm/SPECS/geoserver.spec deleted file mode 100644 index 70d4fe232a7..00000000000 --- a/package/geoserver/rpm/SPECS/geoserver.spec +++ /dev/null @@ -1,92 +0,0 @@ -Name: geonode -Version: 2.0 -Release: alpha1 -Summary: Allows the creation, sharing, and collaborative use of geospatial data. -License: see /usr/share/doc/geonode/copyright -Distribution: Debian -Group: Converted/science -Requires(post): bash -Requires(preun): bash -Conflicts: mod_python - -#%define _rpmdir ../ -%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm - -# repacking jar files takes a long time so we disable it -%define __jar_repack %{nil} - -%description -At its core, the GeoNode has a stack based on GeoServer, pycsw, -Django, and GeoExt that provides a platform for sophisticated -web browser spatial visualization and analysis. Atop this stack, -the project has built a map composer and viewer, tools for -analysis, and reporting tools. - -%build -pushd $GEONODE_EXT_ROOT -mvn clean install -rm -rf $RPM_BUILD_ROOT -mkdir -p $RPM_BUILD_ROOT/usr/share/geonode/geoserver -unzip -d $RPM_BUILD_ROOT/usr/share/geonode/geoserver target/geoserver.war -mkdir -p $RPM_BUILD_ROOT/etc/tomcat6/Catalina/localhost/ -echo '' >> $RPM_BUILD_ROOT/etc/tomcat6/Catalina/localhost/geoserver.xml -mkdir -p $RPM_BUILD_ROOT/usr/share/opengeo-suite/geoserver/WEB-INF/lib/ -cp target/geonode-geoserver-ext-0.3.jar $RPM_BUILD_ROOT/usr/share/opengeo-suite/geoserver/WEB-INF/lib/ - -# GeoServer -%package geoserver -Summary: GeoServer for %{name}. -Group: Development/Libraries -Requires: tomcat6 - -%description geoserver -High performance, standards-compliant map and geospatial data server. -GeoServer is an open source software server written in Java that allows users to share -and edit geospatial data. Contains GeoNode extensions. - -%files geoserver -%defattr(-, root, root, 0755) -/usr/share/geonode/* -/etc/tomcat6/Catalina/localhost/geoserver.xml - -%post geoserver -# Back up tomcat6 config file before changing it -cp /usr/share/tomcat6/conf/tomcat6.conf /usr/share/tomcat6/conf/tomcat6.conf.orig - -#GeoServer needs more ram than the default for tomcat. -JVM_OPTS='JAVA_OPTS="-Djava.awt.headless=true -Xms256m -Xmx768m -Xrs -Dgwc.context.suffix=gwc -XX:PerfDataSamplingInterval=500 -XX:MaxPermSize=128m -DGEOSERVER_CSRF_DISABLED=true"' - -# Append a line with the new jvm configuration -if [ "$(grep ^GEOSERVER /usr/share/tomcat6/conf/tomcat6.conf)" == "" ]; then - echo '# GEOSERVER additions' >> /usr/share/tomcat6/conf/tomcat6.conf - echo 'JAVA_HOME=/usr/' >> /usr/share/tomcat6/conf/tomcat6.conf - echo $JVM_OPTS >> /usr/share/tomcat6/conf/tomcat6.conf -fi - -# Fix permissions on deployed jar -chown -R tomcat:tomcat /usr/share/geonode/geoserver/ - -# start tomcat after installing geoserver -service tomcat6 restart - -%postun geoserver -service tomcat6 restart - -# OpenGeo Suite GeoServer -%package opengeo-geoserver -Summary: %{name} extensions for the OpenGeo Suite. -Group: Development/Libraries -Requires: opengeo-geoserver - -%description opengeo-geoserver -GeoNode extensions for The OpenGeo Suite. - -%files opengeo-geoserver -%defattr(-, root, root, 0755) -/usr/share/opengeo-suite/geoserver/WEB-INF/lib/* - -%post opengeo-geoserver -service tomcat6 restart - -%postun opengeo-geoserver -service tomcat6 restart diff --git a/package/install.sh b/package/install.sh deleted file mode 100755 index ec37fb5a865..00000000000 --- a/package/install.sh +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env bash -# GeoNode installer script -# -# using getopts -# - -if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root" 1>&2 - exit 1 -fi - -while getopts 's:' OPTION -do - case $OPTION in - s) - stepflag=1 - stepval="$OPTARG" - ;; - ?) - printf "Usage: %s: [-s value] configfile\n" $(basename $0) >&2 - exit 2 - ;; - esac -done -shift $(($OPTIND - 1)) - -function setup_directories() { - mkdir -p $GEOSERVER_DATA_DIR - mkdir -p $GEONODE_WWW/static - mkdir -p $GEONODE_WWW/uploaded - mkdir -p $GEONODE_WWW/wsgi - mkdir -p $APACHE_SITES - mkdir -p $GEONODE_BIN - mkdir -p $GEONODE_ETC - mkdir -p $GEONODE_ETC/media - mkdir -p $GEONODE_ETC/templates - mkdir -p $GEONODE_SHARE -} - -function reorganize_configuration() { - cp -rp $INSTALL_DIR/support/geonode.apache $APACHE_SITES/geonode.conf - cp -rp $INSTALL_DIR/support/geonode.wsgi $GEONODE_WWW/wsgi/ - cp -rp $INSTALL_DIR/support/geonode.robots $GEONODE_WWW/robots.txt - cp -rp $INSTALL_DIR/support/geonode.binary $GEONODE_BIN/geonode - cp -rp $INSTALL_DIR/support/packages/*.* $GEONODE_SHARE - cp -rp $INSTALL_DIR/GeoNode*.zip $GEONODE_SHARE - cp -rp $INSTALL_DIR/support/geonode.updateip $GEONODE_BIN/geonode-updateip - cp -rp $INSTALL_DIR/support/geonode.admin $GEONODE_SHARE/admin.json - cp -rp $INSTALL_DIR/support/geonode.local_settings $GEONODE_ETC/local_settings.py - - chmod +x $GEONODE_BIN/geonode - chmod +x $GEONODE_BIN/geonode-updateip -} - -function preinstall() { - setup_directories - reorganize_configuration -} - -function randpass() { - [ "$2" == "0" ] && CHAR="[:alnum:]" || CHAR="[:graph:]" - cat /dev/urandom | tr -cd "$CHAR" | head -c ${1:-32} - echo -} - -function setup_postgres_once() { - su - postgres <&2 - exit 2 -fi - -if [ "$stepflag" ] -then - printf "\tStep: '$stepval specified\n" -else - stepval="all" - echo "heh" -fi - -case $stepval in - pre) - echo "Running GeoNode preinstall ..." - preinstall - ;; - once) - echo "Running GeoNode initial configuration ..." - one_time_setup - ;; - post) - echo "Running GeoNode postinstall ..." - postinstall - ;; - setup_apache_once) - echo "Configuring Apache ..." - setup_apache_once - ;; - all) - echo "Running GeoNode installation ..." - preinstall - one_time_setup - setup_geoserver - postinstall - setup_apache_once - ;; - *) - printf "\tValid values for step parameter are: 'pre', 'post','all'\n" - printf "\tDefault value for step is 'all'\n" - ;; -esac diff --git a/package/rpm/README.rst b/package/rpm/README.rst deleted file mode 100644 index cc3f86d168f..00000000000 --- a/package/rpm/README.rst +++ /dev/null @@ -1,75 +0,0 @@ -CentOS packaging scripts for GeoNode -==================================== - -This repository contains the scripts used to build the .rpm (CentOS) package -for GeoNode. If you are interested in modifying GeoNode itself you may find -http://github.com/GeoNode/geonode more relevant. - -Building --------- - -To produce a .rpm package which can be redistributed: - -* Install the rpm packaging tools:: - - yum install rpmbuild rpm-devtools - -* Run the rpmdev-setuptree tool to set up your user account for building RPMs:: - - rpmdev-setuptree - -* Point the BUILD and SPECS subdirectories of the RPM build tree at your - checkout of this project:: - - rmdir ~/rpmbuild/{BUILD,SPECS} && - ln -s ~/geonode-rpm/{BUILD,SPECS} ~/rpmbuild/ - -* Acquire a GeoNode tar.gz archive (by either building it from sources, or from - http://dev.geonode.org/release/ ) and unpack it into - :file:`geonode-rpm/BUILD/`. - -* Fetch the psycopg2 sources from http://initd.org/psycopg/download/ and place - the tarball in :file:`geonode-rpm/BUILD/deps`. - -* You should now have a directory structure like so:: - - geonode-rpm/ - + BUILD/ - + GeoNode-{version}/ - + deps/ - - psycopg2-2.3.1.tar.gz - + scripts/ - + SPECS/ - - geonode.spec - - opengeo.repo - -* Now you can build the GeoNode RPM by using the ``rpmbuild`` command:: - - rpmbuild -bb ~/rpmbuild/SPECS/geonode.spec - -.. note:: - - Currently, building on CentOS machines requires specifying the --buildroot - option to rpmbuild, like so:: - - rpmbuild -bb ~/rpmbuild/SPECS/geonode.spec \ - --buildroot=/home/rpmbuild/rpmbuild/BUILDROOT/ - -After running ``rpmbuild`` you should have the RPM package one directory level -in the :file:`rpmbuild` directory. - -Installation ------------- - -As described in the GeoNode manual, you can access OpenGeo's YUM repository to -get pre-built GeoNode packages. However, if you want to build a package and -install that instead, you can avoid the need for a repository of your own by -using the following command:: - - yum localinstall geonode-{version}.rpm --nogpgcheck - -As GeoNode depends on software not provided by the main CentOS distribution, -you will still need to enable some third-party repositories. OpenGeo's -repository will mirror all GeoNode dependencies, or you can use -`EPEL`_ and -`ELGIS`_ together. diff --git a/package/rpm/SPECS/geonode.spec b/package/rpm/SPECS/geonode.spec deleted file mode 100644 index e4297137672..00000000000 --- a/package/rpm/SPECS/geonode.spec +++ /dev/null @@ -1,105 +0,0 @@ -Name: geonode -Version: 1.1 -Release: rc1.pre -Summary: Allows the creation, sharing, and collaborative use of geospatial data. -License: see /usr/share/doc/geonode/copyright -Distribution: Debian -Group: Converted/science -Requires(post): bash -Requires(preun): bash -Requires: python26, tomcat5, httpd, python26-virtualenv, python26-mod_wsgi, java-1.6.0-openjdk, postgresql84, postgresql84-server, postgresql84-python, postgresql84-libs, geos, postgresql84-devel, python26-devel, gcc -Conflicts: mod_python - -%define _rpmdir ../ -%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm - -# repacking jar files takes a long time so we disable it -# %define __jar_repack %{nil} # this option doesn't seem to work on CentOS 5 -%define __os_install_post \ - /usr/lib/rpm/redhat/brp-compress \ - %{!?__debug_package:/usr/lib/rpm/redhat/brp-strip %{__strip}}; \ - /usr/lib/rpm/redhat/brp-strip-static-archive %{__strip}; \ - /usr/lib/rpm/redhat/brp-strip-comment-note %{__strip} %{__objdump}; \ - /usr/lib/rpm/brp-python-bytecompile; - -%description -At its core, the GeoNode has a stack based on GeoServer, pycsw, -Django, and GeoExt that provides a platform for sophisticated -web browser spatial visualization and analysis. Atop this stack, -the project has built a map composer and viewer, tools for -analysis, and reporting tools. - - -%install - rm -rf $RPM_BUILD_ROOT - mkdir -p $RPM_BUILD_ROOT/usr/share/geonode - # RELEASE=GeoNode-%{version}- - RELEASE=GeoNode-1.0.1-2011-08-23 - for f in bootstrap.py deploy.ini.ex deploy-libs.txt geonode-webapp.pybundle pavement.py README.rst - do - cp "$RELEASE/$f" "$RPM_BUILD_ROOT"/usr/share/geonode/ - done - cp -rp scripts/* $RPM_BUILD_ROOT/usr/share/geonode/. - - #Deploy Java webapps (WAR files) - TC="$RPM_BUILD_ROOT"/var/lib/tomcat5/webapps/ - GS_DATA="$RPM_BUILD_ROOT"/var/lib/geonode-geoserver-data/ - mkdir -p "$TC" - unzip -qq $RELEASE/geoserver.war -d $TC/geoserver/ - cp -R "$TC"/geoserver/data/ "$GS_DATA" - (cd "$TC"/geoserver/WEB-INF/ && patch -p0) < geoserver.patch - unzip -qq $RELEASE/geonetwork.war -d $TC/geonetwork/ - - #Put Apache config files in place - mkdir -p "$RPM_BUILD_ROOT"/etc/httpd/conf.d/ \ - "$RPM_BUILD_ROOT"/var/www/geonode/wsgi/ \ - "$RPM_BUILD_ROOT"/etc/geonode/ - - cp geonode.conf "$RPM_BUILD_ROOT"/etc/httpd/conf.d/ - cp geonode.wsgi "$RPM_BUILD_ROOT"/var/www/geonode/wsgi/ - cp local_settings.py "$RPM_BUILD_ROOT"/etc/geonode/ - - #Set up virtualenv - mkdir -p "$RPM_BUILD_ROOT"/var/www/geonode/{htdocs,htdocs/media,wsgi/geonode/} - for f in bootstrap.py geonode-webapp.pybundle pavement.py - do - cp "$RELEASE"/$f "$RPM_BUILD_ROOT"/var/www/geonode/wsgi/geonode/ - done - cp deps/psycopg2-2.4.2.tar.gz "$RPM_BUILD_ROOT"/var/www/geonode/wsgi/geonode -%post - -cat << EOF >> /etc/sysconfig/tomcat5 -# Next line added for GeoNode services -JAVA_OPTS="-Xmx1024m -XX:MaxPermSize=256m -XX:CompileCommand=exclude,net/sf/saxon/event/ReceivingContentHandler.startElement" -EOF - -pushd /var/www/geonode/wsgi/geonode/ - python26 bootstrap.py - bin/pip install psycopg2-2.4.2.tar.gz -popd - -ln -s /etc/geonode/local_settings.py /var/www/geonode/wsgi/geonode/src/GeoNodePy/geonode/local_settings.py - -echo "GEONODE: you will need to run /usr/share/geonode/setup.sh to complete this installation" - -%preun - -%postun - -%clean - -%files -%defattr(-,root,root,-) -%dir /usr/share/geonode/* -%dir /var/www/geonode/ -%config /etc/geonode/local_settings.* -%config /etc/httpd/conf.d/geonode.conf -%config /var/www/geonode/wsgi/geonode.wsgi -%config /var/lib/tomcat5/webapps/*/WEB-INF/web.xml -/var/www/geonode/wsgi/geonode/* -%attr(-,tomcat,tomcat) %config %dir /var/lib/geonode-geoserver-data -%attr(-,tomcat,tomcat) %config /var/lib/geonode-geoserver-data/* -%dir /var/lib/tomcat5/webapps/geoserver -/var/lib/tomcat5/webapps/geoserver/* -%attr(-,tomcat,tomcat) %dir /var/lib/tomcat5/webapps/geonetwork -%attr(-,tomcat,tomcat) /var/lib/tomcat5/webapps/geonetwork/* diff --git a/package/rpm/SPECS/opengeo.repo b/package/rpm/SPECS/opengeo.repo deleted file mode 100644 index f73ef70d801..00000000000 --- a/package/rpm/SPECS/opengeo.repo +++ /dev/null @@ -1,5 +0,0 @@ -[opengeo] -name=opengeo -baseurl=http://opengeo:apt78902@yum.opengeo.org/centos/ -enabled=1 -gpgcheck=0 diff --git a/package/support/config-ubuntu.sh b/package/support/config-ubuntu.sh deleted file mode 100644 index bf2bbdd51ac..00000000000 --- a/package/support/config-ubuntu.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -# Location of the GeoNode source -INSTALL_DIR=. -# Location of the target directory, it may be blank -# or something like $(CURDIR)/debian/geonode/ -TARGET_ROOT='' -# Tomcat webapps directory -TOMCAT_WEBAPPS=$TARGET_ROOT/var/lib/tomcat8/webapps -# Geoserver data dir, it will survive removals and upgrades -GEOSERVER_DATA_DIR=$TARGET_ROOT/var/lib/geoserver/geonode-data -# Place where GeoNode media is going to be served -GEONODE_WWW=$TARGET_ROOT/var/www/geonode -# Apache sites directory -APACHE_SITES=$TARGET_ROOT/etc/apache2/sites-available -# Path to preferred location of binaries (might be /usr/sbin for CentOS) -GEONODE_BIN=$TARGET_ROOT/usr/sbin/ -# Path to place miscellaneous patches and scripts used during the install -GEONODE_SHARE=$TARGET_ROOT/usr/share/geonode -# Path to GeoNode configuration and customization -GEONODE_ETC=$TARGET_ROOT/etc/geonode -# Path to GeoNode logging folder -GEONODE_LOG=$TARGET_ROOT/var/log/geonode -# OS preferred way of starting or stopping services -# for example 'service httpd' or '/etc/init.d/apache2' -APACHE_SERVICE="invoke-rc.d apache2" -# sama sama -TOMCAT_SERVICE="invoke-rc.d tomcat8" diff --git a/package/support/geonode.admin b/package/support/geonode.admin deleted file mode 100644 index 903818d674c..00000000000 --- a/package/support/geonode.admin +++ /dev/null @@ -1,18 +0,0 @@ -[{ - "fields": { - "date_joined": "2011-06-09 15:15:27", - "email": "ad@m.in", - "first_name": "", - "groups": [], - "is_active": true, - "is_staff": true, - "is_superuser": true, - "last_login": "2011-06-09 15:45:34", - "last_name": "", - "password": "pbkdf2_sha256$30000$rjuGt0Obn8on$cxF75frIOSaitNklLZ0IJ/VonUW0fwEFVF96o0M+lGc=", - "user_permissions": [], - "username": "admin" - }, - "model": "people.Profile", - "pk": 1000 -}] diff --git a/package/support/geonode.apache b/package/support/geonode.apache deleted file mode 100644 index 379155f3d7d..00000000000 --- a/package/support/geonode.apache +++ /dev/null @@ -1,86 +0,0 @@ -WSGIDaemonProcess geonode user=www-data threads=15 processes=2 - - - Servername localhost - ServerAdmin webmaster@localhost - - LimitRequestFieldSize 32760 - LimitRequestLine 32760 - - ErrorLog /var/log/apache2/error.log - LogLevel warn - CustomLog /var/log/apache2/access.log combined - - WSGIProcessGroup geonode - WSGIPassAuthorization On - WSGIScriptAlias / /var/www/geonode/wsgi/geonode.wsgi - - - Order allow,deny - Options -Indexes - Allow from all - - - Alias /static/ /var/www/geonode/static/ - Alias /uploaded/ /var/www/geonode/uploaded/ - Alias /robots.txt /var/www/geonode/robots.txt - - - Order allow,deny - Deny from all - - - - Order allow,deny - Deny from all - - - - Order allow,deny - Options Indexes FollowSymLinks - Allow from all - Require all granted - IndexOptions FancyIndexing - - - - Order allow,deny - Options Indexes FollowSymLinks - Allow from all - Require all granted - IndexOptions FancyIndexing - - - - Order allow,deny - Options Indexes FollowSymLinks - Allow from all - Require all granted - IndexOptions FancyIndexing - - - - Order allow,deny - Options Indexes FollowSymLinks - Allow from all - Require all granted - IndexOptions FancyIndexing - - - - Order allow,deny - Options Indexes FollowSymLinks - Allow from all - Require all granted - IndexOptions FancyIndexing - - - - Order allow,deny - Allow from all - - - ProxyPreserveHost On - ProxyPass /geoserver http://localhost:8080/geoserver - ProxyPassReverse /geoserver http://localhost:8080/geoserver - diff --git a/package/support/geonode.binary b/package/support/geonode.binary deleted file mode 100644 index 0004c5f17d7..00000000000 --- a/package/support/geonode.binary +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-geonode.local_settings} - -if [ -n "$VIRTUAL_ENV" ]; then - DJANGO_ADMIN_CMD=$VIRTUAL_ENV/bin/django-admin -elif hash django-admin 2>/dev/null; then - DJANGO_ADMIN_CMD=django-admin -else - DJANGO_ADMIN_CMD=django-admin.py -fi - -DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE $DJANGO_ADMIN_CMD "$@" diff --git a/package/support/geonode.local_settings b/package/support/geonode.local_settings deleted file mode 100644 index 6530b168322..00000000000 --- a/package/support/geonode.local_settings +++ /dev/null @@ -1,429 +0,0 @@ -# -*- coding: utf-8 -*- -######################################################################### -# -# Copyright (C) 2018 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 . -# -######################################################################### - -""" There are 3 ways to override GeoNode settings: - 1. Using environment variables, if your changes to GeoNode are minimal. - 2. Creating a downstream project, if you are doing a lot of customization. - 3. Override settings in a local_settings.py file, legacy. -""" - -import ast -import os -from urlparse import urlparse, urlunparse -from geonode.settings import * - -# Setting debug to true makes Django serve static media and -# present pretty error pages. -DEBUG = TEMPLATE_DEBUG = False - -# Set to True to load non-minified versions of (static) client dependencies -# Requires to set-up Node and tools that are required for static development -# otherwise it will raise errors for the missing non-minified dependencies -DEBUG_STATIC = False - -SITENAME = 'GeoNode' -SITEURL = 'http://localhost/' - -# we need hostname for deployed -_surl = urlparse(SITEURL) -HOSTNAME = _surl.hostname - -# add trailing slash to site url. geoserver url will be relative to this -if not SITEURL.endswith('/'): - SITEURL = '{}/'.format(SITEURL) - -ALLOWED_HOSTS = [urlparse(SITEURL).hostname] if os.getenv('ALLOWED_HOSTS') is None \ - else re.split(r' *[,|:|;] *', os.getenv('ALLOWED_HOSTS')) - -DATABASE_ENGINE = 'postgresql_psycopg2' -DATABASE_NAME = 'geonode' -DATABASE_USER = 'geonode' -DATABASE_PASSWORD = 'THE_DATABASE_PASSWORD' -DATABASE_HOST = 'localhost' -DATABASE_PORT = '5432' - -DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': DATABASE_NAME, - 'USER': DATABASE_USER, - 'PASSWORD': DATABASE_PASSWORD, - 'HOST': DATABASE_HOST, - 'PORT': DATABASE_PORT, - 'CONN_MAX_AGE': 0, - 'CONN_TOUT': 5, - 'OPTIONS': { - 'connect_timeout': 5, - } - }, - # vector datastore for uploads - 'datastore': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - # 'ENGINE': '', # Empty ENGINE name disables - 'NAME': 'geonode_data', - 'USER': DATABASE_USER, - 'PASSWORD': DATABASE_PASSWORD, - 'HOST': DATABASE_HOST, - 'PORT': DATABASE_PORT, - 'CONN_MAX_AGE': 0, - 'CONN_TOUT': 5, - 'OPTIONS': { - 'connect_timeout': 5, - } - } -} - -GEOSERVER_LOCATION = os.getenv( - 'GEOSERVER_LOCATION', 'http://localhost:8080/geoserver/' -) - -GEOSERVER_PUBLIC_LOCATION = os.getenv( - 'GEOSERVER_PUBLIC_LOCATION', '{}gs/'.format(SITEURL) -) - -GEOSERVER_ADMIN_USER = os.getenv( - 'GEOSERVER_ADMIN_USER', 'admin' -) - -GEOSERVER_ADMIN_PASSWORD = os.getenv( - 'GEOSERVER_ADMIN_PASSWORD', 'geoserver' -) - -GEOSERVER_FACTORY_PASSWORD = os.getenv( - 'GEOSERVER_FACTORY_PASSWORD', 'geoserver' -) - -# OGC (WMS/WFS/WCS) Server Settings -OGC_SERVER = { - 'default': { - 'BACKEND': 'geonode.geoserver', - 'LOCATION': GEOSERVER_LOCATION, - 'LOGIN_ENDPOINT': 'j_spring_oauth2_geonode_login', - 'LOGOUT_ENDPOINT': 'j_spring_oauth2_geonode_logout', - # PUBLIC_LOCATION needs to be kept like this because in dev mode - # the proxy won't work and the integration tests will fail - # the entire block has to be overridden in the local_settings - 'PUBLIC_LOCATION': GEOSERVER_PUBLIC_LOCATION, - 'USER': GEOSERVER_ADMIN_USER, - 'PASSWORD': GEOSERVER_ADMIN_PASSWORD, - 'MAPFISH_PRINT_ENABLED': True, - 'PRINT_NG_ENABLED': True, - 'GEONODE_SECURITY_ENABLED': True, - 'GEOFENCE_SECURITY_ENABLED': True, - 'WMST_ENABLED': False, - 'BACKEND_WRITE_ENABLED': True, - 'WPS_ENABLED': True, - 'LOG_FILE':'/usr/share/geoserver/data/logs/geoserver.log', - # Set to dictionary identifier of database containing spatial data in DATABASES dictionary to enable - 'DATASTORE': 'datastore', - 'TIMEOUT': 60 # number of seconds to allow for HTTP requests - } -} - -# If you want to enable Mosaics use the following configuration -UPLOADER = { - 'BACKEND': 'geonode.importer', - 'OPTIONS': { - 'TIME_ENABLED': False, - 'MOSAIC_ENABLED': False, - }, - 'SUPPORTED_CRS': [ - 'EPSG:4326', - 'EPSG:3785', - 'EPSG:3857', - 'EPSG:32647', - 'EPSG:32736' - ], - 'SUPPORTED_EXT': [ - '.shp', - '.csv', - '.kml', - '.kmz', - '.json', - '.geojson', - '.tif', - '.tiff', - '.geotiff', - '.gml', - '.xml' - ] -} - -LANGUAGE_CODE = 'en' - -MEDIA_ROOT = '/var/www/geonode/uploaded' -STATIC_ROOT = '/var/www/geonode/static/' - -# secret key used in hashing, should be a long, unique string for each -# site. See http://docs.djangoproject.com/en/1.2/ref/settings/#secret-key -SECRET_KEY = 'THE_SECRET_KEY' - - -CATALOGUE = { - 'default': { - # The underlying CSW implementation - # default is pycsw in local mode (tied directly to GeoNode Django DB) - 'ENGINE': 'geonode.catalogue.backends.pycsw_local', - # pycsw in non-local mode - # 'ENGINE': 'geonode.catalogue.backends.pycsw_http', - # deegree and others - # 'ENGINE': 'geonode.catalogue.backends.generic', - # The FULLY QUALIFIED base url to the CSW instance for this GeoNode - 'URL': '%scatalogue/csw' % SITEURL, - # 'URL': 'http://localhost:8080/geonetwork/srv/en/csw', - # 'URL': 'http://localhost:8080/deegree-csw-demo-3.0.4/services', - 'ALTERNATES_ONLY': True, - } -} - -# pycsw settings -PYCSW = { - # pycsw configuration - 'CONFIGURATION': { - # uncomment / adjust to override server config system defaults - # 'server': { - # 'maxrecords': '10', - # 'pretty_print': 'true', - # 'federatedcatalogues': 'http://catalog.data.gov/csw' - # }, - 'metadata:main': { - 'identification_title': 'GeoNode Catalogue', - 'identification_abstract': 'GeoNode is an open source platform' \ - ' that facilitates the creation, sharing, and collaborative use' \ - ' of geospatial data', - 'identification_keywords': 'sdi, catalogue, discovery, metadata,' \ - ' GeoNode', - 'identification_keywords_type': 'theme', - 'identification_fees': 'None', - 'identification_accessconstraints': 'None', - 'provider_name': 'Organization Name', - 'provider_url': SITEURL, - 'contact_name': 'Lastname, Firstname', - 'contact_position': 'Position Title', - 'contact_address': 'Mailing Address', - 'contact_city': 'City', - 'contact_stateorprovince': 'Administrative Area', - 'contact_postalcode': 'Zip or Postal Code', - 'contact_country': 'Country', - 'contact_phone': '+xx-xxx-xxx-xxxx', - 'contact_fax': '+xx-xxx-xxx-xxxx', - 'contact_email': 'Email Address', - 'contact_url': 'Contact URL', - 'contact_hours': 'Hours of Service', - 'contact_instructions': 'During hours of service. Off on ' \ - 'weekends.', - 'contact_role': 'pointOfContact', - }, - 'metadata:inspire': { - 'enabled': 'true', - 'languages_supported': 'eng,gre', - 'default_language': 'eng', - 'date': 'YYYY-MM-DD', - 'gemet_keywords': 'Utility and governmental services', - 'conformity_service': 'notEvaluated', - 'contact_name': 'Organization Name', - 'contact_email': 'Email Address', - 'temp_extent': 'YYYY-MM-DD/YYYY-MM-DD', - } - } -} - -GEONODE_ROOT = os.path.abspath(os.path.dirname(__file__)) - -TEMPLATE_DIRS = ( - '/etc/geonode/templates', - os.path.join(GEONODE_ROOT, 'templates'), -) - -# Additional directories which hold static files -STATICFILES_DIRS = [ - '/etc/geonode/media', - os.path.join(GEONODE_ROOT, 'static'), -] - -# GeoNode javascript client configuration - -# default map projection -# Note: If set to EPSG:4326, then only EPSG:4326 basemaps will work. -DEFAULT_MAP_CRS = "EPSG:3857" - -DEFAULT_LAYER_FORMAT = "image/png8" - -# Where should newly created maps be focused? -DEFAULT_MAP_CENTER = (0, 0) - -# How tightly zoomed should newly created maps be? -# 0 = entire world; -# maximum zoom is between 12 and 15 (for Google Maps, coverage varies by area) -DEFAULT_MAP_ZOOM = 0 - -# To enable the MapStore2 based Client enable those -INSTALLED_APPS += ('geonode_mapstore_client', ) -GEONODE_CLIENT_HOOKSET = "geonode_mapstore_client.hooksets.MapStoreHookSet" - -MAPBOX_ACCESS_TOKEN = os.environ.get('MAPBOX_ACCESS_TOKEN', None) -BING_API_KEY = os.environ.get('BING_API_KEY', None) -GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY', None) - -if 'geonode.geoserver' in INSTALLED_APPS: - LOCAL_GEOSERVER = { - "source": { - "ptype": "gxp_wmscsource", - "url": OGC_SERVER['default']['PUBLIC_LOCATION'] + "wms", - "restUrl": "/gs/rest" - } - } - baselayers = MAP_BASELAYERS - MAP_BASELAYERS = [LOCAL_GEOSERVER] - MAP_BASELAYERS.extend(baselayers) - -# To enable the MapStore2 based Client enable those -# if 'geonode_mapstore_client' not in INSTALLED_APPS: -# INSTALLED_APPS += ( -# 'mapstore2_adapter', -# 'geonode_mapstore_client',) -# GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY = 'mapstore' # DEPRECATED use HOOKSET instead -# GEONODE_CLIENT_HOOKSET = "geonode_mapstore_client.hooksets.MapStoreHookSet" - -if 'geonode.geoserver' in INSTALLED_APPS: - LOCAL_GEOSERVER = { - "type": "wms", - "url": OGC_SERVER['default']['PUBLIC_LOCATION'] + "wms", - "visibility": True, - "title": "Local GeoServer", - "group": "background", - "format": "image/png8", - "restUrl": "/gs/rest" - } - # baselayers = MAPSTORE_BASELAYERS - # MAPSTORE_BASELAYERS = [LOCAL_GEOSERVER] - # MAPSTORE_BASELAYERS.extend(baselayers) - -# Use kombu broker by default -# REDIS_URL = 'redis://localhost:6379/1' -# BROKER_URL = REDIS_URL -# CELERY_RESULT_BACKEND = REDIS_URL -CELERYD_HIJACK_ROOT_LOGGER = True -CELERYD_CONCURENCY = 1 -# Set this to False to run real async tasks -CELERY_ALWAYS_EAGER = True -CELERYD_LOG_FILE = None -CELERY_REDIRECT_STDOUTS = True -CELERYD_LOG_LEVEL = 1 - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': True, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s %(process)d ' - '%(thread)d %(message)s' - }, - 'simple': { - 'format': '%(message)s', - }, - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'console': { - 'level': 'INFO', - 'class': 'logging.StreamHandler', - 'formatter': 'simple' - }, - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler', - } - }, - "loggers": { - "django": { - "handlers": ["console"], "level": "ERROR", }, - "geonode": { - "handlers": ["console"], "level": "INFO", }, - "geoserver-restconfig.catalog": { - "handlers": ["console"], "level": "ERROR", }, - "owslib": { - "handlers": ["console"], "level": "ERROR", }, - "pycsw": { - "handlers": ["console"], "level": "INFO", }, - "celery": { - "handlers": ["console"], "level": "ERROR", }, - }, -} - -# Additional settings -CORS_ALLOW_ALL_ORIGINS = True - -GEOIP_PATH = "/usr/local/share/GeoIP" - -# add following lines to your local settings to enable monitoring -MONITORING_ENABLED = True - -if MONITORING_ENABLED: - if 'geonode.monitoring' not in INSTALLED_APPS: - INSTALLED_APPS += ('geonode.monitoring',) - if 'geonode.monitoring.middleware.MonitoringMiddleware' not in MIDDLEWARE_CLASSES: - MIDDLEWARE_CLASSES += \ - ('geonode.monitoring.middleware.MonitoringMiddleware',) - MONITORING_CONFIG = None - MONITORING_HOST_NAME = os.getenv("MONITORING_HOST_NAME", HOSTNAME) - MONITORING_SERVICE_NAME = 'geonode' - -#Define email service on GeoNode -EMAIL_ENABLE = True - -if EMAIL_ENABLE: - EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - EMAIL_HOST = 'localhost' - EMAIL_PORT = 25 - EMAIL_HOST_USER = '' - EMAIL_HOST_PASSWORD = '' - EMAIL_USE_TLS = False - EMAIL_USE_SSL = False - DEFAULT_FROM_EMAIL = 'Example.com ' - -# Documents Thumbnails -UNOCONV_ENABLE = True - -if UNOCONV_ENABLE: - UNOCONV_EXECUTABLE = os.getenv('UNOCONV_EXECUTABLE', '/usr/bin/unoconv') - UNOCONV_TIMEOUT = os.getenv('UNOCONV_TIMEOUT', 30) # seconds - -# Advanced Security Workflow Settings -ACCOUNT_APPROVAL_REQUIRED = False -CLIENT_RESULTS_LIMIT = 20 -API_LIMIT_PER_PAGE = 1000 -FREETEXT_KEYWORDS_READONLY = False -RESOURCE_PUBLISHING = False -ADMIN_MODERATE_UPLOADS = False -GROUP_PRIVATE_RESOURCES = False -GROUP_MANDATORY_RESOURCES = False -MODIFY_TOPICCATEGORY = True -USER_MESSAGES_ALLOW_MULTIPLE_RECIPIENTS = True -DISPLAY_WMS_LINKS = True -CREATE_LAYER = True - -# For more information on available settings please consult the Django docs at -# https://docs.djangoproject.com/en/dev/ref/settings diff --git a/package/support/geonode.robots b/package/support/geonode.robots deleted file mode 100644 index a70544bb34d..00000000000 --- a/package/support/geonode.robots +++ /dev/null @@ -1,4 +0,0 @@ -User-agent: * -Disallow: /geoserver/ -Disallow: /geonetwork/ -EOF diff --git a/package/support/geonode.updateip b/package/support/geonode.updateip deleted file mode 100644 index 990b79d58c2..00000000000 --- a/package/support/geonode.updateip +++ /dev/null @@ -1,212 +0,0 @@ -#!/bin/bash - -usage () { - bname="$(basename $0)" - ret="$1" - - cat < - substitute SITEURL with , append it - to ALLOWED_HOSTS in local_settings.py - and geoserver printing config lists - This is mandatory and must be the pubic extenral address you use to access GeoNode - $bname <-l|--local> - same as above but taking into account - LOCAL vs PUBLIC ip addresses (or names) - This is optional and if not specified the default is 'localhost' - $bname <-s|--secure> - HTTPS instead of HTTP protocol - $bname <-h|--help> - this help -EOF - - exit $ret -} - -if [[ $# -eq 0 ]] ; then - usage 0 - exit 0 -fi - -POSITIONAL=() -IS_HTTPS=0 -while [[ $# -gt 0 ]] -do -key="$1" - -case $key in - -h|--help) - usage 0 - ;; - -p|--public) - PUBLIC_IP="$2" - shift # past argument - shift # past value - ;; - -l|--local) - LOCAL_IP="$2" - shift # past argument - shift # past value - ;; - -s|--secure) - IS_HTTPS=1 - shift # past argument - ;; - *) # unknown option - POSITIONAL+=("$1") # save it in an array for later - shift # past argument - ;; -esac -done -set -- "${POSITIONAL[@]}" # restore positional parameters - -if [[ $UID != 0 ]]; then - echo "Please run this script with sudo:" - echo "sudo $0 $*" - exit 1 -fi - -if [ -z "$PUBLIC_IP" ]; then - echo "Public IP is mandatory!" - usage 1 -fi - -if [ -z "$LOCAL_IP" ]; then - # LOCAL_IP=$PUBLIC_IP - LOCAL_IP="localhost" -fi - -NET_PROTOCOL="http" -if [[ $IS_HTTPS != 0 ]]; then - NET_PROTOCOL="https" -fi - -# Getting the full netloc -NEW_EXT_IP="$NET_PROTOCOL://$PUBLIC_IP" -NEW_INT_IP="$NET_PROTOCOL://$LOCAL_IP" - -# Removing slash at the end of variables -NEW_EXT_IP=${NEW_EXT_IP%/} -NEW_INT_IP=${NEW_INT_IP%/} - -echo "PUBLIC_IP" $NEW_EXT_IP -echo "LOCAL_IP" $NEW_INT_IP - -GEONODE_ETC=${GEONODE_ETC:-/etc/geonode} -GEOSERVER_DATA_DIR=${GEOSERVER_DATA_DIR:-/usr/share/geoserver/data} -TOMCAT_SERVICE=${TOMCAT_SERVICE:-"invoke-rc.d tomcat8"} -APACHE_SERVICE=${APACHE_SERVICE:-"invoke-rc.d apache2"} - -# Replace SITEURL in $GEONODE_ETC/local_settings.py -echo "Replacing SITEURL value with '$NEW_EXT_IP' in $GEONODE_ETC/local_settings.py ... " | tr -d '\n' -sed -i "s@\(SITEURL[ ]*=[ ]*\).*@\1\'$NEW_EXT_IP\/'@g" $GEONODE_ETC/local_settings.py -echo "done." - -echo "Adding entry for '$PUBLIC_IP' in $GEOSERVER_DATA_DIR/printing/config.yaml ... " | tr -d '\n' -printing_config=$GEOSERVER_DATA_DIR/printing/config.yaml - -if grep -q "$PUBLIC_IP" "$printing_config" -then - echo "'$PUBLIC_IP' already found to the printing whitelist." -else - sed -i "s#hosts:#hosts:\n - !ipMatch\n ip: $PUBLIC_IP#g" $printing_config - echo "done." -fi - -# if ALLOWED_HOSTS already exists ... -# if grep -q "^[ ]*ALLOWED_HOSTS[ ]*=" "$GEONODE_ETC/local_settings.py" -# then -# if [ $IS_REPLACE -eq 1 ] -# then -# echo "Replacing ALLOWED_HOSTS in $GEONODE_ETC/local_settings.py ... " | tr -d '\n' -# sed -i "s/^\([ ]*ALLOWED_HOSTS[ ]*=\).*/\1 [ 'localhost', '$NEWIP', ]/g" "$GEONODE_ETC/local_settings.py" -# echo "done." -# else -# echo "Adding $NEWIP to ALLOWED_HOSTS in $GEONODE_ETC/local_settings.py ... " | tr -d '\n' -# items="$(grep "^[ ]*ALLOWED_HOSTS[ ]*=" "$GEONODE_ETC/local_settings.py" | \ -# sed 's/^[ ]*ALLOWED_HOSTS[ ]*=[ ]*\[//g;s/\][ ]*$//g')" -# already_found=0 -# oldifs="$IFS" -# IFS=',' -# for item in $items -# do -# item_cls="$(echo "$item" | sed "s/^[ ]*['\"]//g;s/['\"][ ]*$//g")" -# if [ "$item_cls" = "$NEWIP" ] -# then -# already_found=1 -# break -# fi -# done -# IFS="$oldifs" -# if [ $already_found -eq 0 ] -# then -# if echo "$items" | grep -q ',[ ]*$' -# then -# items="${items}'$NEWIP', " -# else -# items="${items}, '$NEWIP', " -# fi -# sed -i "s/^\([ ]*ALLOWED_HOSTS[ ]*=\).*/\1 [ $items ]/g" "$GEONODE_ETC/local_settings.py" -# echo "done." -# else -# echo "'$NEWIP' already found in ALLOWED_HOSTS list." -# fi -# fi -# else -# echo "Adding ALLOWED_HOSTS with in $GEONODE_ETC/local_settings.py ... " | tr -d '\n' -# echo "ALLOWED_HOSTS=['localhost', '$NEWIP', ]" >> $GEONODE_ETC/local_settings.py -# echo "done." -# fi - -# silence the warnings from executing geonode command or they will pollute the commands output -if grep -q "^[ ]*SILENCED_SYSTEM_CHECKS[ ]*=" "$GEONODE_ETC/local_settings.py" -then - true -else - echo "SILENCED_SYSTEM_CHECKS = ['1_8.W001', 'fields.W340', 'auth.W004', 'urls.W002']" >> $GEONODE_ETC/local_settings.py -fi - -geonode fixsitename - -echo "Setting up oauth" -# Set oauth keys -oauth_keys=$(geonode fixoauthuri -f --target-address $NEW_EXT_IP 2>&1) -client_id=`echo $oauth_keys | cut -d \, -f 1` -client_secret=`echo $oauth_keys | cut -d \, -f 2` - -# Updating OAuth2 Service Config -oauth_config="$GEOSERVER_DATA_DIR/security/filter/geonode-oauth2/config.xml" -sed -i "s|.*|$client_id|g" $oauth_config -sed -i "s|.*|$client_secret|g" $oauth_config -sed -i "s|.*|$NEW_EXT_IP/o/token/|g" $oauth_config -sed -i "s|.*|$NEW_EXT_IP/o/authorize/|g" $oauth_config -sed -i "s|.*|$NEW_EXT_IP/geoserver/|g" $oauth_config -sed -i "s|.*|$NEW_EXT_IP/api/o/v4/tokeninfo/|g" $oauth_config -sed -i "s|.*|$NEW_EXT_IP/account/logout/|g" $oauth_config - -# Updating REST Role Service Config -sed -i "s|.*|$NEW_EXT_IP|g" "$GEOSERVER_DATA_DIR/security/role/geonode REST role service/config.xml" - -# Updating GeoServer Global Config -global_config="$GEOSERVER_DATA_DIR/global.xml" -sed -i "s|.*|$NEW_EXT_IP/geoserver|g" $global_config - -# Restart tomcat server -$TOMCAT_SERVICE restart - -echo "Waiting Tomcat Service to Restart..." -sleep 30s - -# Restart apache server -$APACHE_SERVICE restart -echo "Waiting Apache HTTPD Service to Restart..." -sleep 5s - -# Run updatelayers -geonode updatelayers -geonode set_all_datasets_alternate -geonode set_all_datasets_metadata -d - -# Run updatemaplayerip -geonode updatemaplayerip diff --git a/package/support/geonode.wsgi b/package/support/geonode.wsgi deleted file mode 100644 index f8fd7c25d12..00000000000 --- a/package/support/geonode.wsgi +++ /dev/null @@ -1,7 +0,0 @@ -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "geonode.local_settings") - -application = get_wsgi_application() diff --git a/package/support/geoserver.patch b/package/support/geoserver.patch deleted file mode 100644 index f969b502c43..00000000000 --- a/package/support/geoserver.patch +++ /dev/null @@ -1,20 +0,0 @@ ---- web.xml 2011-08-29 19:25:04.000000000 +0000 -+++ web.xml.new 2011-08-29 19:50:48.000000000 +0000 -@@ -39,7 +39,16 @@ - PARTIAL_BUFFER_STRATEGY_SIZE - 50 - -- -+ -+ -+ GEONODE_BASE_URL -+ http://localhost/ -+ -+ -+ -+ GEOSERVER_DATA_DIR -+ /var/lib/geoserver/geonode-data/ -+ - J=wc$QYa`>m;4tjqlbsJ|8V)8`p*wH=u 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 z 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⁢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

qQ%XX_S6oi2rZUNkl=T{B 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,V4E6`5t%isT7@C~pJm{T) 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

dC-qxkC09!P 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

pg@ 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�W-PmQFr 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 z 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�CJn5>kl^O(2=P$o$W;xAVT1Y%WmB|SIzPPx7w8JDf5w`_=& zqi%BWAPDya56P!PA3erPKPj7${R*q*-l(Wa&ZP;z@VyNmx6HiYyRhI37DS&z5l6aD zcKuY~u$}L_E!>aYuA9}JEuA%W{_WkC-yMB3_BHl@iVwAs8r=wwsgYBVrO|bBjd74) z_r||Z<`7wF;DGTssUctrB)M`5m$_7!>AFVAg*Ai%BErbFXl@*KUy8`Jb*a(ms214~ z@c0|2YXEvZ-<5ZM{J(#SZQ9ULVTEd8r@oTv+|F)}!!=qof4w#ol+g3*$mdaJr|`qr z=^}os?H-Nv^ZEwW{RZP4P}aX}!L9P8N7lPc1Y7+vqi-$!gz?Yx;=|GDb@6Hu@ zGM|Z^IlubCrvoZ@Dp9=^AK~M&uHLKgngWq!%f9y?LN5l7$&f7&qktGHW4t1HBoMfI zLWAb?v9|gM_C9CTDkLU#_mpY;jhJS4OvYJ1;8x(Rff|Z0e9%;%#2g*h9v_e@L^8BF z1mP<@+?gtd;gqr=GTRa?UCO?+yin3F@dRDwa1nhTLX$Ig?s>v#=bW;S${%+J8sCKb zXe}}>2kW8}J^bWeh02f@fowD2Z0gum`(l`Tm71ry_3!w;4BDfM^2^ywN`^_ZMXq1c zMIQBoT8=Ju>KlV9&T+gRnEhLW%{T6scaJ>1!w1WE1a+1(xHN`dqs38Gr*R^An-*sF z)oo(l+ynyK5=~&}aj2OIb^aBnveYE?d}Vl4PCO@LS+HH6RElPsHY2RJ`S;{JIG&d` zVBqL3uJ@ghl;gaQ+VfxOyKmg;|Xdosapucxou)2-L(q{QQ!)g2Zha}}g0Qh^Yap-2W{ z^F}rmSp9$q0_y14ako7|C`@=5;f_83Gh7$z!=H+-UsK0Q7N7U9u8-(o z2@Qg>k+QQH%^n%VD!wqgHQH-I<;%cvE+Qw4?C3cgwOwQQ@mY`RT;f77jS}0xmg)X z%isy#x!iNAuk%?A=Nt0l^k-1{Q5(mjNPjjF6Cu+SUD^*ZSxjK!`?L_(Ndb2~T?cFt z6pXXV?mss`TQd`u(z`jdPY0Tb~9M2wQ19sTku^gGy@W_C>I z4QqBSZ(1Y0i_DMxrH!b}-+H?1tQRXko!xDp+uOoVW;5TIL9T$`t)9LQ^4#FArdBsG z+5ZNdI@XIj-%n7?=;6<4`t~4S68m62U0=8=Wp_i`txIT=3r-Rh|+Z=1j2mUpZFZO&JFk`DGu*FNDUPUa}kkr}UsEczD zpFIzRf;$e)Z2yqQHTw!}d%xRYuK`?k8C6)!Y-i#hb*cJ?(!fylit+TJ%}mU2vp(tiIg zyZy`mH$InPB~h(CA9`WI&IMN-yZl7az4fbRDNZ~80nl}q+P~UCg2eDNX$xGZ7Y4z^ zzFN#B`_~X>gYR1WQyRCSb=WbE@$YF)moAKhfYa1twP7R4d;putty=)rdlHmXiN?Y8cFnad$Fw9xhw1@ zY?^U$j8;FRbjwnc3&n0qeB1ed;uX!PXuLJO^CyU$8zV7XUT8m#p3FjhBdFlEHZ43SStk-}NLQ@@X6!jQYti+fG*&mUT3ZeuI?w(!SWg+*z-9=1g({H>DfJn@P6le_|$9C|jBmFRRrNhRMyxm|KWd(2?z632u$UB+?zb;Vt; zI`STA5lTZjNh24c1S{Q9 oAvurax-FBPg)!2-7axc{B7W2fIlvT-c+eC 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%0DJeB`AFLq%8Xxy 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 z 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 @`3fd!G)SLVP02Phasl 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

  • <_Z1NVP*izD(QzeR{tPAHOIVknVb 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

      p|7jEqIU*wWd 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

      _+GS@&%|ClpLmN~{b%a28@ma3 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

      &3@E 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

      WF9do*RmOUbv8I*Ep-_wQAGm(4yfg&Z$3_ozp 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 Vm{&?r5oNLE7xg!ut4|$v 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≀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

      PVv7R%Jb)VsnGKHr&Z*me=Z~Ch<>abH~djP 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