diff --git a/geonode/metadata/api/views.py b/geonode/metadata/api/views.py index de1a4e2d83b..a7959172313 100644 --- a/geonode/metadata/api/views.py +++ b/geonode/metadata/api/views.py @@ -36,6 +36,7 @@ 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.handlers.abstract import MetadataHandler from geonode.metadata.i18n import get_localized_label from geonode.metadata.manager import metadata_manager from geonode.people.utils import get_available_users @@ -77,7 +78,7 @@ def schema(self, request, pk=None): # Handle the JSON schema instance @action( detail=False, - methods=["get", "put"], + methods=["get", "put", "patch"], url_path=r"instance/(?P\d+)", url_name="schema_instance", permission_classes=[ @@ -86,6 +87,7 @@ def schema(self, request, pk=None): "default": { "GET": ["base.view_resourcebase"], "PUT": ["change_resourcebase_metadata"], + "PATCH": ["change_resourcebase_metadata"], } } ) @@ -102,13 +104,25 @@ def schema_instance(self, request, pk=None): schema_instance, content_type="application/schema-instance+json", json_dumps_params={"indent": 3} ) - elif request.method == "PUT": + elif request.method in ("PUT", "PATCH"): 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, lang) + errors = {} + try: + errors = ( + metadata_manager.update_schema_instance(resource, request, lang) + if request.method == "PUT" + else metadata_manager.update_schema_instance_partial(resource, request.data, request.user, lang) + ) + resource.save() # we want to trigger all the post_save signals + except Exception as e: + logger.warning(f"Error while updating schema instance: {e}") + MetadataHandler._set_error( + errors, [], MetadataHandler.localize_message({}, "metadata_error_save", {"exc": e}) + ) msg_t = ( ("m_metadata_update_error", "Some errors were found while updating the resource") diff --git a/geonode/metadata/handlers/abstract.py b/geonode/metadata/handlers/abstract.py index 1fa065f5970..bdb83582c4d 100644 --- a/geonode/metadata/handlers/abstract.py +++ b/geonode/metadata/handlers/abstract.py @@ -156,7 +156,7 @@ def _localize_label(context, lang: str, text: str): @staticmethod def _get_tkl_labels(context, lang: str | None, text: str): - return context["labels"].get(text, None) + return context.get("labels", {}).get(text, None) @staticmethod def _localize_subschema_labels(context, subschema: dict, lang: str, property_name: str = None): diff --git a/geonode/metadata/handlers/base.py b/geonode/metadata/handlers/base.py index 7fb280a861c..c560f631d13 100644 --- a/geonode/metadata/handlers/base.py +++ b/geonode/metadata/handlers/base.py @@ -226,6 +226,7 @@ def update_resource(self, resource, field_name, json_instance, context, errors, field_value = SUBHANDLERS[field_name].deserialize(field_value) setattr(resource, field_name, field_value) + context.setdefault("base", {})[field_name] = field_value except Exception as e: logger.warning(f"Error setting field {field_name}={field_value}: {e}") self._set_error( diff --git a/geonode/metadata/handlers/doi.py b/geonode/metadata/handlers/doi.py index 2175a1750ec..efa4ad9ecab 100644 --- a/geonode/metadata/handlers/doi.py +++ b/geonode/metadata/handlers/doi.py @@ -47,3 +47,4 @@ def get_jsonschema_instance(self, resource, field_name, context, errors, lang=No def update_resource(self, resource, field_name, json_instance, context, errors, **kwargs): resource.doi = json_instance.get(field_name, None) + context.setdefault("base", {})[field_name] = json_instance.get(field_name, None) diff --git a/geonode/metadata/manager.py b/geonode/metadata/manager.py index e494c9e238c..81a4ef2da61 100644 --- a/geonode/metadata/manager.py +++ b/geonode/metadata/manager.py @@ -115,8 +115,7 @@ def build_schema_instance(self, resource, lang=None): return instance - def update_schema_instance(self, resource, request_obj, lang=None) -> dict: - + def update_schema_instance(self, resource, request_obj, lang=None, partial=None) -> dict: # Definition of the json instance json_instance = request_obj.data @@ -134,6 +133,11 @@ def update_schema_instance(self, resource, request_obj, lang=None) -> dict: errors = {} for fieldname, subschema in schema["properties"].items(): + if partial: + if fieldname not in partial: + continue + else: + logger.debug(f"Storing partial field {fieldname}") handler = self.handlers[subschema["geonode:handler"]] try: handler.update_resource(resource, fieldname, json_instance, context, errors) @@ -160,7 +164,10 @@ def update_schema_instance(self, resource, request_obj, lang=None) -> dict: ), ) try: - resource.save() + resource.get_real_concrete_instance_class().objects.filter(id=resource.id).update( + **context.setdefault("base") + ) + resource.refresh_from_db() except Exception as e: logger.warning(f"Error while updating schema instance: {e}") MetadataHandler._set_error( @@ -185,6 +192,16 @@ def update_schema_instance(self, resource, request_obj, lang=None) -> dict: return errors + def update_schema_instance_partial(self, resource, json_instance, user, lang=None) -> dict: + # We can't loop on the payload's field, since post_ or pre_ methods may rely on the whole instance + # Let's create a full instance by using the old one, merged with the payload + old_instance = self.build_schema_instance(resource, lang) + old_instance.update(json_instance) + fake_req = lambda: None + fake_req.data = old_instance + fake_req.user = user + return self.update_schema_instance(resource, fake_req, lang, partial=json_instance.keys()) + def _create_test_errors(schema, errors, path, msg_template, create_message=True): if create_message: diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 5b386437059..f7c616e7232 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -38,12 +38,14 @@ from django.utils.module_loading import import_string from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError, FieldDoesNotExist +from tomli import _re - +from geonode.assets.utils import create_asset_and_link_dict, rollback_asset_and_link, copy_assets_and_links, create_link from geonode.base.models import ResourceBase, LinkedResource +from geonode.documents.tasks import create_document_thumbnail +from geonode.metadata.manager import metadata_manager from geonode.thumbs.thumbnails import _generate_thumbnail_name from geonode.thumbs.utils import ThumbnailAlgorithms -from geonode.documents.tasks import create_document_thumbnail from geonode.security.permissions import PermSpecCompact, DATA_STYLABLE_RESOURCES_SUBTYPES from geonode.security.utils import ( perms_as_set, @@ -54,7 +56,6 @@ from . import settings as rm_settings from .utils import update_resource, resourcebase_post_save -from geonode.assets.utils import create_asset_and_link_dict, rollback_asset_and_link, copy_assets_and_links, create_link from ..base import enumerations from ..security.utils import AdvancedSecurityWorkflowManager @@ -360,6 +361,10 @@ def update( vals=vals, extra_metadata=extra_metadata, ) + + if ji:=custom.get("jsoninstance", None): + metadata_manager.update_schema_instance_partial(_resource, ji, user=None) + _resource = self._concrete_resource_manager.update(uuid, instance=_resource, notify=notify) # The following is only a demo proof of concept for a pluggable WF subsystem