Skip to content

fields#

check_child_model_serializer#

Source code in src/apps/common/serializers/fields.py
def check_child_model_serializer(child):
    assert isinstance(
        child, serializers.ModelSerializer
    ), "The `child` argument should be a model serializer."

    assert "url" in child.fields, "The `child` serializer should have a `url` field."

URLReferencedModelListField#

Bases: ListField

Custom field for a model list.

Allows user to represent targets using the 'url' key.

The 'child' parameter in 'serializers.ListField' is used for serializing the list in the response.

Source code in src/apps/common/serializers/fields.py
class URLReferencedModelListField(serializers.ListField):
    """Custom field for a model list.

    Allows user to represent targets using the 'url' key.

    The 'child' parameter in 'serializers.ListField' is used for serializing
    the list in the response.
    """

    def __init__(self, **kwargs):
        check_child_model_serializer(kwargs.get("child"))
        super().__init__(**kwargs)

    def to_representation(self, manager):
        if isinstance(manager, list):
            return [self.child.to_representation(entry) for entry in manager]
        else:
            return [self.child.to_representation(entry) for entry in manager.all()]

    def to_internal_value(self, data):
        if not isinstance(data, list) or isinstance(data, (str, dict)):
            self.fail("not_a_list", input_type=type(data).__name__)

        if data and hasattr(data[0], "url"):
            # The list of entries has already been converted into model
            # instances
            return data

        try:
            urls = set(entry["url"] for entry in data)
        except KeyError:
            raise serializers.ValidationError(
                "'url' field must be defined for each object in the list"
            )
        except TypeError:
            raise serializers.ValidationError(
                "Each item in the list must be an object with the field 'url'"
            )

        model = self.child.Meta.model
        entries = list(model.objects.filter(url__in=urls))

        retrieved_urls = set(entry.url for entry in entries)
        missing_urls = urls - retrieved_urls

        if missing_urls:
            model_name = self.child.Meta.model.__name__
            raise serializers.ValidationError(
                "{model_name} entries not found for given URLs: {urls}".format(
                    model_name=model_name, urls=", ".join(missing_urls)
                )
            )

        return entries

URLReferencedModelField#

Bases: RelatedField

Serialized RelatedField for URL-referenced concepts.

Accepts input data in the format { "url": ... } which is then used to fetch a model instance. The instance is serialized using a model serializer. The serializer is expected to have a "url" field.

The field is essentially a wrapper around a concept serializer. Using a non-serializer field here prevents the parent serializer from complaining about unsupported writable nested serializers.

Source code in src/apps/common/serializers/fields.py
class URLReferencedModelField(serializers.RelatedField):
    """Serialized RelatedField for URL-referenced concepts.

    Accepts input data in the format
    {
        "url": ...
    }
    which is then used to fetch a model instance.
    The instance is serialized using a model serializer.
    The serializer is expected to have a "url" field.

    The field is essentially a wrapper around a concept serializer.
    Using a non-serializer field here prevents the parent serializer
    from complaining about unsupported writable nested serializers.
    """

    default_error_messages = {
        "does_not_exist": _("{model_name} entry not found for url '{url_value}'."),
    }

    def __init__(self, child: serializers.ModelSerializer, **kwargs):
        check_child_model_serializer(child)
        self.queryset = child.Meta.model.objects.all()
        self.child = child
        # Always allow child to be null. Otherwise both parent and child would require allow_null,
        # e.g. `URLReferencedModelField(Child(allow_null=True), allow_null=True)`
        child.allow_null = True
        self.child.bind(field_name="", parent=self)
        super().__init__(**kwargs)

    @classmethod
    def many_init(cls, **kwargs):
        """For many=True use custom list field that retrieves multiple urls at once."""
        return URLReferencedModelListField(**kwargs)

    def run_validation(self, data=empty):
        try:
            if data is not empty:
                data = self.child.run_validation(data)
            return serializers.Field.run_validation(self, data)
        except serializers.ValidationError as exc:
            raise serializers.ValidationError(exc.detail)

    def to_internal_value(self, data):
        try:
            return self.get_queryset().get(url=data["url"])
        except ObjectDoesNotExist:
            model_name = self.child.Meta.model.__name__
            self.fail("does_not_exist", url_value=data["url"], model_name=model_name)

    def to_representation(self, value):
        return self.child.to_representation(value)

    def get_value(self, dictionary):
        """Convert DRF forms input from json to dict."""
        value = super().get_value(dictionary)
        if html.is_html_input(dictionary):
            value = json.loads(value)
        return value

    def get_choices(self, cutoff=None):
        """Modified get_choices that converts dict to json for DRF forms."""
        queryset = self.get_queryset()
        if queryset is None:
            return {}

        if cutoff is not None:
            queryset = queryset[:cutoff]

        return OrderedDict(
            [
                (json.dumps(self.to_representation(item)), self.display_value(item))
                for item in queryset
            ]
        )

get_choices(cutoff=None) #

Modified get_choices that converts dict to json for DRF forms.

Source code in src/apps/common/serializers/fields.py
def get_choices(self, cutoff=None):
    """Modified get_choices that converts dict to json for DRF forms."""
    queryset = self.get_queryset()
    if queryset is None:
        return {}

    if cutoff is not None:
        queryset = queryset[:cutoff]

    return OrderedDict(
        [
            (json.dumps(self.to_representation(item)), self.display_value(item))
            for item in queryset
        ]
    )

get_value(dictionary) #

Convert DRF forms input from json to dict.

Source code in src/apps/common/serializers/fields.py
def get_value(self, dictionary):
    """Convert DRF forms input from json to dict."""
    value = super().get_value(dictionary)
    if html.is_html_input(dictionary):
        value = json.loads(value)
    return value

many_init(**kwargs) classmethod #

For many=True use custom list field that retrieves multiple urls at once.

Source code in src/apps/common/serializers/fields.py
@classmethod
def many_init(cls, **kwargs):
    """For many=True use custom list field that retrieves multiple urls at once."""
    return URLReferencedModelListField(**kwargs)

ChecksumField#

Bases: CharField

Source code in src/apps/common/serializers/fields.py
class ChecksumField(serializers.CharField):
    allowed_algorithms = ["md5", "sha256", "sha512"]

    default_error_messages = {
        "invalid": _(
            "Checksum should be a lowercase string in format 'algorithm:value'. "
            "Allowed algorithms are: {allowed_algorithms}."
        )
    }

    @property
    def checksum_regex(self):
        return rf"^({'|'.join(self.allowed_algorithms)}):[a-z0-9_]+$"

    def fail(self, key, **kwargs):
        kwargs["allowed_algorithms"] = self.allowed_algorithms
        return super().fail(key, **kwargs)

    def __init__(self, **kwargs):
        kwargs["trim_whitespace"] = True
        super().__init__(**kwargs)
        validator = RegexValidator(
            self.checksum_regex,
            message=self.error_messages["invalid"].format(
                allowed_algorithms=self.allowed_algorithms
            ),
        )
        self.validators.append(validator)

RemoteResourceChecksumField#

Bases: ChecksumField

Source code in src/apps/common/serializers/fields.py
class RemoteResourceChecksumField(ChecksumField):
    allowed_algorithms = ChecksumField.allowed_algorithms + ["sha1", "sha224", "sha384", "other"]

ListValidChoicesField#

Bases: ChoiceField

ChoiceField that lists valid choices in the 'invalid choice' error message.

Source code in src/apps/common/serializers/fields.py
class ListValidChoicesField(serializers.ChoiceField):
    """ChoiceField that lists valid choices in the 'invalid choice' error message."""

    def __init__(self, *args, **kwargs):
        choices = kwargs.get("choices", [])
        kwargs["error_messages"] = {
            "invalid_choice": serializers.ChoiceField.default_error_messages["invalid_choice"]
            + " "
            + _("Valid choices are: {choices}").format(
                choices=[c for c in to_choices_dict(choices)]
            ),
            **kwargs.get("error_messages", {}),
        }

        super().__init__(*args, **kwargs)

MediaTypeField#

Bases: CharField

Source code in src/apps/common/serializers/fields.py
class MediaTypeField(serializers.CharField):
    default_error_messages = {"invalid": _("Value should contain a media type, e.g. 'text/csv'.")}

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        validator = MediaTypeValidator(message=self.error_messages["invalid"])
        self.validators.append(validator)

WKTField#

Bases: CharField

Serializer field that accepts a WKT string and normalizes it.

Source code in src/apps/common/serializers/fields.py
class WKTField(serializers.CharField):
    """Serializer field that accepts a WKT string and normalizes it."""

    def to_internal_value(self, data):
        try:
            return shapely.wkt.loads(data).wkt
        except shapely.errors.GEOSException as error:
            raise serializers.ValidationError(_("Invalid WKT: {}").format(str(error)))

MultiLanguageField#

Bases: HStoreField

Serializer field for MultiLanguageField model fields.

Languages with null or "" as translation are removed from the object. Disallows empty objects {} by default.

Source code in src/apps/common/serializers/fields.py
class MultiLanguageField(serializers.HStoreField):
    """Serializer field for MultiLanguageField model fields.

    Languages with `null` or "" as translation are removed from the object.
    Disallows empty objects `{}` by default.
    """

    child = serializers.CharField(allow_blank=True, allow_null=True)

    def to_internal_value(self, data):
        if isinstance(data, dict):
            data = {
                lang: translation
                for lang, translation in data.items()
                if translation not in [None, ""]
            }
        if data == {} and self.allow_null and not self.allow_empty:
            return None
        return super().to_internal_value(data)

    def __init__(self, **kwargs):
        kwargs.setdefault("allow_empty", False)
        super().__init__(**kwargs)

NullableCharField#

Bases: CharField

CharField that converts empty strings to nulls if allowed.

Source code in src/apps/common/serializers/fields.py
class NullableCharField(serializers.CharField):
    """CharField that converts empty strings to nulls if allowed."""

    def run_validation(self, data):
        # CharField checks for empty string in run_validation
        # instead of to_internal_value.
        if self.allow_null:
            is_empty = data == "" or (self.trim_whitespace and str(data).strip() == "")
            if is_empty:
                data = None
        return super().run_validation(data)

PrivateEmailField#

Bases: EmailField

Email field that is hidden by CommonModelSerializer by default.

Source code in src/apps/common/serializers/fields.py
class PrivateEmailField(serializers.EmailField):
    """Email field that is hidden by CommonModelSerializer by default."""