def apply_changes(self, obj, requested_changes): """Merge `changes` into `object` fields. .. note:: This is used in the context of PATCH only. Override this to control field changes at object level, for example: .. code-block:: python def apply_changes(self, obj, requested_changes): # Ignore value change if inferior if object['position'] > changes.get('position', -1): changes.pop('position', None) return super().apply_changes(obj, requested_changes) :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest` if result does not comply with resource schema. :returns: the new object with `changes` applied. :rtype: tuple """ if self._is_json_patch: try: applied_changes = apply_json_patch(obj, requested_changes)["data"] updated = {**applied_changes} except ValueError as e: error_details = { "location": "body", "description": f"JSON Patch operation failed: {e}", } raise_invalid(self.request, **error_details) else: applied_changes = {**requested_changes} updated = {**obj} # recursive patch and remove field if null attribute is passed (RFC 7396) if self._is_merge_patch: recursive_update_dict(updated, applied_changes, ignores=(None,)) else: updated.update(**applied_changes) for field, value in applied_changes.items(): has_changed = obj.get(field, value) != value if self.schema.is_readonly(field) and has_changed: error_details = {"name": field, "description": f"Cannot modify {field}"} raise_invalid(self.request, **error_details) try: validated = self.schema().deserialize(updated) except colander.Invalid as e: # Transform the errors we got from colander into Cornice errors. # We could not rely on Service schema because the object should be # validated only once the changes are applied for field, error in e.asdict().items(): # pragma: no branch raise_invalid(self.request, name=field, description=error) return validated, applied_changes
def process_record(self, new, old=None): """Read permissions from request body, and in the case of ``PUT`` every existing ACE is removed (using empty list). """ new = super().process_record(new, old) # patch is specified as a list of of operations (RFC 6902) payload = self.request.validated['body'] if self._is_json_patch: permissions = apply_json_patch(old, payload)['permissions'] elif self._is_merge_patch: existing = old or {} permissions = existing.get('__permissions__', {}) recursive_update_dict(permissions, payload.get('permissions', {}), ignores=(None,)) else: permissions = {k: v for k, v in payload.get('permissions', {}).items() if v is not None} annotated = {**new} if permissions: is_put = (self.request.method.lower() == 'put') if is_put or self._is_merge_patch: # Remove every existing ACEs using empty lists. for perm in self.permissions: permissions.setdefault(perm, []) annotated[self.model.permissions_field] = permissions return annotated
def process_record(self, new, old=None): """Read permissions from request body, and in the case of ``PUT`` every existing ACE is removed (using empty list). """ new = super().process_record(new, old) # patch is specified as a list of of operations (RFC 6902) if self._is_json_patch: changes = self.request.validated['body'] permissions = apply_json_patch(old, changes)['permissions'] else: permissions = self.request.validated['body'].get('permissions', {}) annotated = {**new} if permissions: is_put = (self.request.method.lower() == 'put') if is_put: # Remove every existing ACEs using empty lists. for perm in self.permissions: permissions.setdefault(perm, []) annotated[self.model.permissions_field] = permissions return annotated
def apply_changes(self, record, requested_changes): """Merge `changes` into `record` fields. .. note:: This is used in the context of PATCH only. Override this to control field changes at record level, for example: .. code-block:: python def apply_changes(self, record, requested_changes): # Ignore value change if inferior if record['position'] > changes.get('position', -1): changes.pop('position', None) return super().apply_changes(record, requested_changes) :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest` if result does not comply with resource schema. :returns: the new record with `changes` applied. :rtype: tuple """ if self._is_json_patch: try: applied_changes = apply_json_patch(record, requested_changes)['data'] updated = {**applied_changes} except ValueError as e: error_details = { 'location': 'body', 'description': 'JSON Patch operation failed: {}'.format(e) } raise_invalid(self.request, **error_details) else: applied_changes = {**requested_changes} updated = {**record} content_type = str(self.request.headers.get('Content-Type')).lower() # recursive patch and remove field if null attribute is passed (RFC 7396) if content_type == 'application/merge-patch+json': recursive_update_dict(updated, applied_changes, ignores=[None]) else: updated.update(**applied_changes) for field, value in applied_changes.items(): has_changed = record.get(field, value) != value if self.schema.is_readonly(field) and has_changed: error_details = { 'name': field, 'description': 'Cannot modify {}'.format(field) } raise_invalid(self.request, **error_details) try: validated = self.schema().deserialize(updated) except colander.Invalid as e: # Transform the errors we got from colander into Cornice errors. # We could not rely on Service schema because the record should be # validated only once the changes are applied for field, error in e.asdict().items(): # pragma: no branch raise_invalid(self.request, name=field, description=error) return validated, applied_changes
def process_object(self, new, old=None): """Hook for processing objects before they reach storage, to introduce specific logics on fields for example. .. code-block:: python def process_object(self, new, old=None): new = super().process_object(new, old) version = old['version'] if old else 0 new['version'] = version + 1 return new Or add extra validation based on request: .. code-block:: python from kinto.core.errors import raise_invalid def process_object(self, new, old=None): new = super().process_object(new, old) if new['browser'] not in request.headers['User-Agent']: raise_invalid(self.request, name='browser', error='Wrong') return new :param dict new: the validated object to be created or updated. :param dict old: the old object to be updated, ``None`` for creation endpoints. :returns: the processed object. :rtype: dict """ modified_field = self.model.modified_field new_last_modified = new.get(modified_field) # Drop the new last_modified if it is not an integer. is_integer = isinstance(new_last_modified, int) if not is_integer: new.pop(modified_field, None) new_last_modified = None # Drop the new last_modified if lesser or equal to the old one. is_less_or_equal = ( new_last_modified and old is not None and new_last_modified <= old[modified_field] ) if is_less_or_equal: new.pop(modified_field, None) # patch is specified as a list of of operations (RFC 6902) payload = self.request.validated["body"] if self._is_json_patch: permissions = apply_json_patch(old, payload)["permissions"] elif self._is_merge_patch: existing = old or {} permissions = existing.get("__permissions__", {}) recursive_update_dict(permissions, payload.get("permissions", {}), ignores=(None,)) else: permissions = { k: v for k, v in payload.get("permissions", {}).items() if v is not None } annotated = {**new} if permissions: is_put = self.request.method.lower() == "put" if is_put or self._is_merge_patch: # Remove every existing ACEs using empty lists. for perm in self.permissions: permissions.setdefault(perm, []) annotated[self.model.permissions_field] = permissions return annotated
def apply_changes(self, record, requested_changes): """Merge `changes` into `record` fields. .. note:: This is used in the context of PATCH only. Override this to control field changes at record level, for example: .. code-block:: python def apply_changes(self, record, requested_changes): # Ignore value change if inferior if record['position'] > changes.get('position', -1): changes.pop('position', None) return super().apply_changes(record, requested_changes) :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest` if result does not comply with resource schema. :returns: the new record with `changes` applied. :rtype: tuple """ if self._is_json_patch: try: applied_changes = apply_json_patch(record, requested_changes)['data'] updated = {**applied_changes} except ValueError as e: error_details = { 'location': 'body', 'description': 'JSON Patch operation failed: {}'.format(e) } raise_invalid(self.request, **error_details) else: applied_changes = {**requested_changes} updated = {**record} # recursive patch and remove field if null attribute is passed (RFC 7396) if self._is_merge_patch: recursive_update_dict(updated, applied_changes, ignores=(None,)) else: updated.update(**applied_changes) for field, value in applied_changes.items(): has_changed = record.get(field, value) != value if self.schema.is_readonly(field) and has_changed: error_details = { 'name': field, 'description': 'Cannot modify {}'.format(field) } raise_invalid(self.request, **error_details) try: validated = self.schema().deserialize(updated) except colander.Invalid as e: # Transform the errors we got from colander into Cornice errors. # We could not rely on Service schema because the record should be # validated only once the changes are applied for field, error in e.asdict().items(): # pragma: no branch raise_invalid(self.request, name=field, description=error) return validated, applied_changes