diff --git a/geonode/catalogue/backends/generic.py b/geonode/catalogue/backends/generic.py index 9482497a337..f90ea420d56 100644 --- a/geonode/catalogue/backends/generic.py +++ b/geonode/catalogue/backends/generic.py @@ -30,9 +30,11 @@ from geonode.catalogue.backends.base import BaseCatalogueBackend from geonode.metadata.manager import metadata_manager + logger = logging.getLogger(__name__) TIMEOUT = 10 + METADATA_FORMATS = { "Atom": ("atom:entry", "http://www.w3.org/2005/Atom"), "DIF": ("dif:DIF", "http://gcmd.gsfc.nasa.gov/Aboutus/xml/dif/"), @@ -40,10 +42,25 @@ "ebRIM": ("rim:RegistryObject", "urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0"), "FGDC": ("fgdc:metadata", "http://www.opengis.net/cat/csw/csdgm"), "ISO": ("gmd:MD_Metadata", "http://www.isotc211.org/2005/gmd"), + "ISO19115-3_2018": ("mdb:MD_Metadata", "http://standards.iso.org/iso/19115/-3/mdb/2.0") } -class Catalogue(CatalogueServiceWeb): +class DefaultMetadataFormatMixin: + @property + def default_format(self): + return settings.CATALOGUE_DEFAULT_FORMAT + + @property + def default_root_node(self): + return METADATA_FORMATS[settings.CATALOGUE_DEFAULT_FORMAT][0] + + @property + def default_schema(self): + return METADATA_FORMATS[settings.CATALOGUE_DEFAULT_FORMAT][1] + + +class Catalogue(CatalogueServiceWeb, DefaultMetadataFormatMixin): def __init__(self, *args, **kwargs): self.url = kwargs["URL"] self.user = None @@ -180,7 +197,7 @@ def search(self, keywords, startposition, maxrecords, bbox): constraints=dataset_query_like + bbox_query, startposition=startposition, maxrecords=maxrecords, - outputschema="http://www.isotc211.org/2005/gmd", + outputschema=self.default_schema, esn="full", ) @@ -226,7 +243,7 @@ def metadatarecord2dict(self, rec): # construct the link to the Catalogue metadata record (not # self-indexed) result["metadata_links"] = [ - ("text/xml", "ISO", self.url_for_uuid(rec.identifier, "http://www.isotc211.org/2005/gmd")) + ("text/xml", self.default_format, self.url_for_uuid(rec.identifier, self.default_schema)) ] return result @@ -255,7 +272,7 @@ def extract_links(self, rec): return links -class CatalogueBackend(BaseCatalogueBackend): +class CatalogueBackend(BaseCatalogueBackend, DefaultMetadataFormatMixin): def __init__(self, *args, **kwargs): self.catalogue = Catalogue(*args, **kwargs) @@ -302,6 +319,6 @@ def create_record(self, item): record = self.catalogue.get_by_uuid(item.uuid) if record is None: md_link = self.catalogue.create_from_dataset(item) - item.metadata_links = [("text/xml", "ISO", md_link)] + item.metadata_links = [("text/xml", self.default_format, md_link)] else: self.catalogue.update_dataset(item) diff --git a/geonode/catalogue/backends/pycsw_http.py b/geonode/catalogue/backends/pycsw_http.py index 0058c41359c..2b2323f89ce 100644 --- a/geonode/catalogue/backends/pycsw_http.py +++ b/geonode/catalogue/backends/pycsw_http.py @@ -17,8 +17,7 @@ # ######################################################################### -from geonode.catalogue.backends.generic import CatalogueBackend as GenericCatalogueBackend - +from geonode.catalogue.backends.generic import CatalogueBackend as GenericCatalogueBackend, METADATA_FORMATS class CatalogueBackend(GenericCatalogueBackend): """pycsw HTTP CSW backend""" @@ -26,4 +25,4 @@ class CatalogueBackend(GenericCatalogueBackend): def __init__(self, *args, **kwargs): """initialize pycsw HTTP CSW backend""" GenericCatalogueBackend.__init__(CatalogueBackend, self, *args, **kwargs) - self.catalogue.formats = ["Atom", "DIF", "Dublin Core", "ebRIM", "FGDC", "ISO"] + self.catalogue.formats = list(METADATA_FORMATS.keys()) diff --git a/geonode/catalogue/backends/pycsw_local.py b/geonode/catalogue/backends/pycsw_local.py index a8ff5034954..93306289730 100644 --- a/geonode/catalogue/backends/pycsw_local.py +++ b/geonode/catalogue/backends/pycsw_local.py @@ -20,12 +20,20 @@ import os from owslib.etree import etree as dlxml from django.conf import settings -from owslib.iso import MD_Metadata from pycsw import server from geonode.catalogue.backends.generic import CatalogueBackend as GenericCatalogueBackend from geonode.catalogue.backends.generic import METADATA_FORMATS from shapely.errors import ShapelyError +match settings.CATALOGUE_DEFAULT_FORMAT: + case "ISO": + from owslib.iso import MD_Metadata + case "ISO19115-3_2018": + from owslib.iso3 import MD_Metadata + case _: + raise ValueError(f"Unsuported metadata format: {settings.CATALOGUE_DEFAULT_FORMAT}") + + true_value = "true" false_value = "false" if settings.DATABASES["default"]["ENGINE"].endswith( @@ -66,7 +74,7 @@ class CatalogueBackend(GenericCatalogueBackend): def __init__(self, *args, **kwargs): GenericCatalogueBackend.__init__(CatalogueBackend, self, *args, **kwargs) - self.catalogue.formats = ["Atom", "DIF", "Dublin Core", "ebRIM", "FGDC", "ISO"] + self.catalogue.formats = list(METADATA_FORMATS.keys()) self.catalogue.local = True def remove_record(self, uuid): @@ -80,7 +88,7 @@ def get_record(self, uuid): if len(results) < 1: return None - result = dlxml.fromstring(results).find("{http://www.isotc211.org/2005/gmd}MD_Metadata") + result = dlxml.fromstring(results).find("{%s}MD_Metadata" % self.default_schema) if result is None: return None @@ -103,7 +111,7 @@ def search_records(self, keywords, start, limit, bbox): e = dlxml.fromstring(lresults) self.catalogue.records = [ - MD_Metadata(x) for x in e.findall("//{http://www.isotc211.org/2005/gmd}MD_Metadata") + MD_Metadata(x) for x in e.findall("//{%s}MD_Metadata" % self.default_schema) ] # build results into JSON for API @@ -150,7 +158,7 @@ def _csw_local_dispatch(self, keywords=None, start=0, limit=10, bbox=None, ident "typenames": formats, "resulttype": "results", "constraintlanguage": "CQL_TEXT", - "outputschema": "http://www.isotc211.org/2005/gmd", + "outputschema": self.default_schema, "constraint": None, "startposition": start, "maxrecords": limit, @@ -162,7 +170,7 @@ def _csw_local_dispatch(self, keywords=None, start=0, limit=10, bbox=None, ident "version": "2.0.2", "request": "GetRecordById", "id": identifier, - "outputschema": "http://www.isotc211.org/2005/gmd", + "outputschema": self.default_schema, } # FIXME(Ariel): Remove this try/except block when pycsw deals with # empty geometry fields better. diff --git a/geonode/catalogue/backends/tests.py b/geonode/catalogue/backends/tests.py index f579b68872b..10d8742e0d7 100644 --- a/geonode/catalogue/backends/tests.py +++ b/geonode/catalogue/backends/tests.py @@ -7,6 +7,12 @@ from geonode.catalogue.views import csw_global_dispatch from django.test import TestCase from django.conf import settings +from urllib.parse import urlencode + +from .generic import METADATA_FORMATS + + +SCHEMA = urlencode(METADATA_FORMATS[settings.CATALOGUE_DEFAULT_FORMAT][1]) pycsw_settings = settings.PYCSW.copy() pycsw_settings_all = settings.PYCSW.copy() @@ -50,7 +56,7 @@ def test_if_pycsw_filter_is_set_should_return_all_datasets_map_doc(self): def __request_factory(): factory = RequestFactory() url = "http://localhost:8000/catalogue/csw?request=GetRecords" - url += "&service=CSW&version=2.0.2&outputschema=http%3A%2F%2Fwww.isotc211.org%2F2005%2Fgmd" + url += f"&service=CSW&version=2.0.2&outputschema={SCHEMA}" url += "&elementsetname=brief&typenames=csw:Record&resultType=results" request = factory.get(url) diff --git a/geonode/catalogue/models.py b/geonode/catalogue/models.py index e575be179e6..306b794bc5c 100644 --- a/geonode/catalogue/models.py +++ b/geonode/catalogue/models.py @@ -94,7 +94,13 @@ def catalogue_post_save(instance, sender, **kwargs): LOGGER.exception(e) csw_anytext = "" - resources.update(metadata_xml=md_doc, csw_wkt_geometry=instance.geographic_bounding_box, csw_anytext=csw_anytext) + resources.update( + csw_typename=catalogue.default_root_node, + csw_schema=catalogue.default_schema, + metadata_xml=md_doc, + csw_wkt_geometry=instance.geographic_bounding_box, + csw_anytext=csw_anytext + ) if "geonode.catalogue" in settings.INSTALLED_APPS: diff --git a/geonode/catalogue/templates/catalogue/full_metadata_iso19115-3.xml b/geonode/catalogue/templates/catalogue/full_metadata_iso19115-3.xml new file mode 100644 index 00000000000..7abf9b68aec --- /dev/null +++ b/geonode/catalogue/templates/catalogue/full_metadata_iso19115-3.xml @@ -0,0 +1,657 @@ +{% load thesaurus %} +{% load l10n %} + + + + + {{layer.uuid}} + + + urn:uuid + + + + + + + + + + + + + + + + + + + + + {% for contact_role in layer.contactrole_set.all %} + {% with role=contact_role.role contact=contact_role.contact %} + + + + + + + + + {% else %}>{{ contact.organization }}{% endif %} + + + + + {% else %}>{{ contact.get_full_name }}{% endif %} + + + {% else %}>{{ contact.position }}{% endif %} + + + + {% if contact.voice %} + + + + {{ contact.voice }} + + + voice + + + + {% endif %} + {% if contact.fax %} + + + + {{ contact.fax }} + + + fax + + + + {% endif %} + + + + {% else %}>{{ contact.delivery }}{% endif %} + + + {% else %}>{{ contact.city }}{% endif %} + + + {% else %}>{{ contact.area }}{% endif %} + + + {% else %}>{{ contact.zipcode }}{% endif %} + + + {% else %}>{{ contact.country }}{% endif %} + + + {% else %}>{{ contact.email }}{% endif %} + + + + + + + {{ SITEURL }}{{ contact.get_absolute_url }} + + + Profile + + + WWW:LINK-1.0-http--link + + + GeoNode profile page + + + + + + + + + + + + {% endwith %} + {% endfor %} + + + + {{layer.csw_insert_date|date:"Y-m-d\TH:i:s\Z"}} + + + creation + + + + + + + ISO 19115-3:2014 + + + + + + + ISO 19115-3:2014 + + + + + + + {{ SITEURL }}{{ layer.get_absolute_url }} + + + + + + + {% comment %} + + + + + + {{ 4674 }} + + + {{ EPSG }} + + + + + + {% endcomment %} + + + + + + + {{layer.title}} + + {% if layer.alternate %} + + {{layer.alternate}} + + {% endif %} + + + + {{layer.date|date:"Y-m-d\TH:i:s\Z"}} + + + {{layer.date_type}} + + + + {% if layer.edition %} + + {{layer.edition}} + + {% endif %} + {% if layer.doi %} + + + + doi:{{ layer.doi }} + + + + doi + + + Digital Object Identifier (DOI) + + + {% endif %} + + mapDigital + + + + + {{ layer.raw_abstract }} + + + {% else %}>{{ layer.raw_purpose }}{% endif %} + + + + + {% with layer.owner as owner %} + + + + + + + + + {% else %}>{{ owner.organization }}{% endif %} + + + + + {% else %}>{{ owner.get_full_name }}{% endif %} + + + {% else %}>{{ owner.position }}{% endif %} + + + + {% if not owner.voice %} + + + + {{ owner.voice }} + + + voice + + + + {% endif %} + {% if not owner.fax %} + + + + {{ owner.fax }} + + + voice + + + + {% endif %} + + + + {% else %}>{{ owner.delivery }}{% endif %} + + + {% else %}>{{ owner.city }}{% endif %} + + + {% else %}>{{ owner.area }}{% endif %} + + + {% else %}>{{ owner.zipcode }}{% endif %} + + + {% else %}>{{ owner.country }}{% endif %} + + + {% else %}>{{ owner.email }}{% endif %} + + + + + + + {{ SITEURL }}{{ owner.get_absolute_url }} + + + Profile + + + WWW:LINK-1.0-http--link + + + GeoNode profile page + + + + + + + + + + + + {% endwith %} + {% if layer.maintenance_frequency %} + + + + {{layer.maintenance_frequency}} + + + + {% endif %} + + + + {{ layer.get_thumbnail_url }} + + + Thumbnail for '{{ layer.title }}' + + + image/png + + + + {% if layer.spatial_representation_type %} + + + + {% endif %} + {% comment %} + + + + + + + + + + + + {% endcomment %} + {% if layer.category %} + + {{ layer.category.identifier }} + + {% endif %} + + + + + {% localize off %} + + {{ layer.ll_bbox.0 }} + + + {{ layer.ll_bbox.1 }} + + + {{ layer.ll_bbox.2 }} + + + {{ layer.ll_bbox.3 }} + + {% endlocalize %} + + + + + {% if layer.keyword_list %} + + + {% for kw in layer.keyword_list %} + + {{ kw }} + + {% endfor %} + + theme + + + + {% endif %} + {% if layer.tkeywords %} + {% for thesaurus_id in layer.tkeywords|get_unique_thesaurus_set %} + + + {% for keyword in layer.tkeywords.all|with_localized_labels %} + {% if keyword.thesaurus.id == thesaurus_id %} + + {{ keyword.localized_label }} + + {% endif %} + {% endfor %} + + + + {{ thesaurus_id|get_thesaurus_title }} + + + + + {{ thesaurus_id|get_thesaurus_date }} + + + publication + + + + + + + + {% endfor %} + {% endif %} + {% if layer.regions.exists %} + + + {% for region in layer.regions.all %} + + {{ region.name }} + + {% endfor %} + + place + + + + {% endif %} + + + + + + + + + + + {% if layer.raw_supplemental_information %} + + {{ layer.raw_supplemental_information }} + + {% endif %} + {% if LICENSES_METADATA == 'light' and layer.license %} + + + + license + + + {{layer.license_light}} + + {% if layer.doi %} + + DOI: {{layer.doi}} + + {% endif %} + + + {% endif %} + {% if LICENSES_METADATA == 'verbose' and layer.license %} + + + + license + + {{layer.license_verbose}} + + + + {% endif %} + + + + {{ layer.restriction_code_type.identifier }} + + + {{layer.raw_constraints_other}} + + + + + + {% if layer.raw_data_quality_statement %} + + + {{ layer.raw_data_quality_statement }} + + + + + {{ layer.get_csw_type_display }} + + + + + {% endif %} + + {% if layer.subtype == 'raster' %} + + + + image + + + {% elif layer.subtype == 'vector' %} + + + 0 + + + + {% endif %} + + + + + + + + + {{ SITEURL }}{% url 'resolve_uuid' uuid=layer.uuid %} + + + WWW:LINK-1.0-http--link + + + {{ layer.alternate }} + + + Online link to the '{{ layer.title }}' description on GeoNode + + + + + + + {% for link in layer.link_set.download %} + + + + {{ link.url }} + + + WWW:DOWNLOAD-1.0-http--download + + + {{ layer.name }}.{{ link.extension }} + + + {{ layer.title }} ({{ link.name }} Format) + + + + + + + {% endfor %} + {% for link in layer.link_set.ows %} + + + + {{ link.url }} + + + {{ link.link_type }} + + + {{ layer.alternate }} + + + {{layer.workspace}} Service - Provides Layer: {{ layer.title }} + + + + + + + {% endfor %} + + + + + \ No newline at end of file diff --git a/geonode/settings.py b/geonode/settings.py index 5ade041ddb9..fbf948adc6b 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -1204,6 +1204,18 @@ } } +# Metadata format +CATALOGUE_DEFAULT_FORMAT=os.getenv("CATALOGUE_DEFAULT_FORMAT", "ISO") + +METADATA_TEMPLATES = { + "ISO": "catalogue/full_metadata.xml", + "ISO19115-3_2018": "catalogue/full_metadata_iso19115-3.xml", +} +if CATALOGUE_DEFAULT_FORMAT not in METADATA_TEMPLATES: + raise ValueError(f"Metadata profile invalid: {CATALOGUE_DEFAULT_FORMAT}") + +CATALOG_METADATA_TEMPLATE = os.getenv("CATALOG_METADATA_TEMPLATE", METADATA_TEMPLATES[CATALOGUE_DEFAULT_FORMAT]) + # pycsw settings PYCSW = { # pycsw configuration @@ -1223,8 +1235,8 @@ "pretty_print": "true", # 'domainquerytype': 'range', "domaincounts": "true", - "profiles": "apiso,ebrim", }, + "profiles": {"apiso", "ebrim", "iso19115p3"}, "manager": { # authentication/authorization is handled by Django "transactions": "false", @@ -2159,8 +2171,6 @@ def get_geonode_catalogue_service(): # Metadata Wizard TOPICCATEGORY_MANDATORY = ast.literal_eval(os.environ.get("TOPICCATEGORY_MANDATORY", "False")) -CATALOG_METADATA_TEMPLATE = os.getenv("CATALOG_METADATA_TEMPLATE", "catalogue/full_metadata.xml") - DEFAULT_AUTO_FIELD = "django.db.models.AutoField" """