Example #1
0
    def import_extra_data(self, obj, extra_data, fields):
        """Import new extra_data content from the client.

        There are three methods for injecting new content into the object's
        ``extra_data`` JSON field:

        1. Simple key/value forms through setting
           :samp:`extra_data.{key}={value}`. This will convert boolean-like
           strings to booleans, numeric strings to integers or floats, and the
           rest are stored as strings. It's only intended for very simple data.

        2. A JSON Merge Patch document through setting
           :samp:`extra_data:json={patch}`. This is a simple way of setting new
           structured JSON content.

        3. A more complex JSON Patch document through setting
           :samp:`extra_data:json-patch={patch}`. This is a more advanced way
           of manipulating JSON data, allowing for sanity-checking of existing
           content, adding new keys/array indices, replacing existing
           keys/indices, deleting data, or copying/moving data. If any
           operation (including the sanity-checking) fails, the whole patch is
           aborted.

        All methods respect any access states that apply to the resource, and
        forbid both writing to keys starting with ``__`` and replacing the
        entire root of ``extra_data``.

        .. versionchanged:: 3.0

           Added support for ``extra_data:json`` and ``extra_data:json-patch``.

        Args:
            obj (django.db.models.Model):
                The object containing an ``extra_data`` field.

            extra_data (dict):
                The existing contents of the ``extra_data`` field. This will
                be updated directly.

            fields (dict):
                The fields being set in the request. This will be checked for
                ``extra_data:json``, ``extra_data:json-patch``, and any
                beginning with ``extra_data.``.

        Returns:
            bool:
            ``True`` if ``extra_data`` was at all modified. ``False`` if it
            wasn't.

        Raises:
            ImportExtraDataError:
                There was an error importing content into ``extra_data``. There
                may be a parse error or access error. Details are in the
                message.
        """
        updated = False

        # Check for a JSON Merge Patch. This is the simplest way to update
        # extra_data with new structured JSON content.
        if 'extra_data:json' in fields:
            try:
                patch = json.loads(fields['extra_data:json'])
            except ValueError as e:
                raise ImportExtraDataError(
                    _('Could not parse JSON data: %s') % e)

            new_extra_data = json_merge_patch(
                extra_data,
                patch,
                can_write_key_func=lambda path, **kwargs: self.
                _can_write_extra_data_key(obj, path))

            # Save extra_data only if it remains a dictionary, so callers
            # can't replace the entire contents.
            if not isinstance(new_extra_data, dict):
                raise ImportExtraDataError(
                    _('extra_data:json cannot replace extra_data with a '
                      'non-dictionary type'))

            extra_data.clear()
            extra_data.update(new_extra_data)
            updated = True

        # Check for a JSON Patch. This is more advanced, and can be used in
        # conjunction with the JSON Merge Patch.
        if 'extra_data:json-patch' in fields:
            try:
                patch = json.loads(fields['extra_data:json-patch'])
            except ValueError as e:
                raise ImportExtraDataError(
                    _('Could not parse JSON data: %s') % e)

            try:
                new_extra_data = json_patch(
                    extra_data,
                    patch,
                    can_read_key_func=self._can_read_extra_data_key,
                    can_write_key_func=lambda path, **kwargs: self.
                    _can_write_extra_data_key(obj, path))

                extra_data.clear()
                extra_data.update(new_extra_data)
                updated = True
            except JSONPatchError as e:
                raise ImportExtraDataError(
                    _('Failed to patch JSON data: %s') % e)

        # Support setting individual keys to simple values. This is the older
        # method of setting JSON data, and is no longer recommended for new
        # clients.
        for key, value in fields.items():
            if key.startswith('extra_data.'):
                key = key[EXTRA_DATA_LEN:]

                if self._can_write_extra_data_key(obj, (key, )):
                    if value != '':
                        if value in ('true', 'True', 'TRUE'):
                            value = True
                        elif value in ('false', 'False', 'FALSE'):
                            value = False
                        else:
                            try:
                                value = int(value)
                            except ValueError:
                                try:
                                    value = float(value)
                                except ValueError:
                                    pass

                        extra_data[key] = value
                        updated = True
                    elif key in extra_data:
                        del extra_data[key]
                        updated = True

        return updated
Example #2
0
    def import_extra_data(self, obj, extra_data, fields):
        """Import new extra_data content from the client.

        There are three methods for injecting new content into the object's
        ``extra_data`` JSON field:

        1. Simple key/value forms through setting
           :samp:`extra_data.{key}={value}`. This will convert boolean-like
           strings to booleans, numeric strings to integers or floats, and the
           rest are stored as strings. It's only intended for very simple data.

        2. A JSON Merge Patch document through setting
           :samp:`extra_data:json={patch}`. This is a simple way of setting new
           structured JSON content.

        3. A more complex JSON Patch document through setting
           :samp:`extra_data:json-patch={patch}`. This is a more advanced way
           of manipulating JSON data, allowing for sanity-checking of existing
           content, adding new keys/array indices, replacing existing
           keys/indices, deleting data, or copying/moving data. If any
           operation (including the sanity-checking) fails, the whole patch is
           aborted.

        All methods respect any access states that apply to the resource, and
        forbid both writing to keys starting with ``__`` and replacing the
        entire root of ``extra_data``.

        .. versionchanged:: 3.0

           Added support for ``extra_data:json`` and ``extra_data:json-patch``.

        Args:
            obj (django.db.models.Model):
                The object containing an ``extra_data`` field.

            extra_data (dict):
                The existing contents of the ``extra_data`` field. This will
                be updated directly.

            fields (dict):
                The fields being set in the request. This will be checked for
                ``extra_data:json``, ``extra_data:json-patch``, and any
                beginning with ``extra_data.``.

        Raises:
            ImportExtraDataError:
                There was an error importing content into ``extra_data``. There
                may be a parse error or access error. Details are in the
                message.
        """
        # Check for a JSON Merge Patch. This is the simplest way to update
        # extra_data with new structured JSON content.
        if 'extra_data:json' in fields:
            try:
                patch = json.loads(fields['extra_data:json'])
            except ValueError as e:
                raise ImportExtraDataError(_('Could not parse JSON data: %s')
                                           % e)

            new_extra_data = json_merge_patch(
                extra_data,
                patch,
                can_write_key_func=lambda path, **kwargs:
                    self._can_write_extra_data_key(obj, path))

            # Save extra_data only if it remains a dictionary, so callers
            # can't replace the entire contents.
            if not isinstance(new_extra_data, dict):
                raise ImportExtraDataError(
                    _('extra_data:json cannot replace extra_data with a '
                      'non-dictionary type'))

            extra_data.clear()
            extra_data.update(new_extra_data)

        # Check for a JSON Patch. This is more advanced, and can be used in
        # conjunction with the JSON Merge Patch.
        if 'extra_data:json-patch' in fields:
            try:
                patch = json.loads(fields['extra_data:json-patch'])
            except ValueError as e:
                raise ImportExtraDataError(_('Could not parse JSON data: %s')
                                           % e)

            try:
                new_extra_data = json_patch(
                    extra_data,
                    patch,
                    can_read_key_func=self._can_read_extra_data_key,
                    can_write_key_func=lambda path, **kwargs:
                        self._can_write_extra_data_key(obj, path))

                extra_data.clear()
                extra_data.update(new_extra_data)
            except JSONPatchError as e:
                raise ImportExtraDataError(_('Failed to patch JSON data: %s')
                                           % e)

        # Support setting individual keys to simple values. This is the older
        # method of setting JSON data, and is no longer recommended for new
        # clients.
        for key, value in six.iteritems(fields):
            if key.startswith('extra_data.'):
                key = key[EXTRA_DATA_LEN:]

                if self._can_write_extra_data_key(obj, (key,)):
                    if value != '':
                        if value in ('true', 'True', 'TRUE'):
                            value = True
                        elif value in ('false', 'False', 'FALSE'):
                            value = False
                        else:
                            try:
                                value = int(value)
                            except ValueError:
                                try:
                                    value = float(value)
                                except ValueError:
                                    pass

                        extra_data[key] = value
                    elif key in extra_data:
                        del extra_data[key]