Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature proposal: "augmented list" #69

Open
antonagestam opened this issue Oct 14, 2021 · 0 comments
Open

Feature proposal: "augmented list" #69

antonagestam opened this issue Oct 14, 2021 · 0 comments

Comments

@antonagestam
Copy link
Contributor

antonagestam commented Oct 14, 2021

I've recently stumbled into a simliar pattern twice where I want to configure a list such that one can pass in partial objects that only contain a primary key, but have fleshed out objects in the response. In the example below we're creating (or PUT-ing) an object that has relations to already existing entities, and we don't want to allow the client to alter the related entities in this request, but we want the client to conveniently be able to get the full objects in the response.

Example request

{
    "related_entities": [
        {"id": 1},
        {"id": 123},
    ]
}

Example response

{
    "related_entities": [
        {"id": 1, "name": "foo"},
        {"id": 123, "name": "bar"},
    ]
}

Solution

For lack of a better name, I'm calling this an "augmented list" for now. Suggestions are welcome.

class AugmentedList(ListField):
    def __init__(self, child: Serializer, method_name: str | None = None, writable_field: str = "id") -> None:
        super().__init__(source="*", child=child, default=list)
        self.method_name = method_name
        self.writable_field = writable_field

    def to_internal_value(  # type: ignore[override]
        self, data: Iterable[dict[str, int]]
    ) -> dict[str, Iterable[int]]:
        assert self.field_name is not None
        return {self.field_name: [item[self.writable_field] for item in data]}
    
    # Stolen from SerializerMethodField
    def bind(self, field_name, parent):
        # The method name defaults to `get_{field_name}`.
        if self.method_name is None:
            self.method_name = 'get_{field_name}'.format(field_name=field_name)
        super().bind(field_name, parent)

    # Stolen from SerializerMethodField and modified to use self.child for serialization and to not pass
    # any value to the method.
    def to_representation(self, value):
        method = getattr(self.parent, self.method_name)
        return [
            type(self.child)(instance=child_instance).data
            for child_instance in method()
        ]

The above specialized list field allows us to achieve the behavior we want, and would be used like this:

class TagSerializer(Serializer):
    id = IntegerField()
    name = CharField()

class HasTags(Serializer):
    tags = AugmentedList(child=TagSerializer())

    def get_tags(self):
        return Tag.objects.filter(...=self.instance.pk)

(The above code is untested as-is, but all the concepts are fully working in separate places at the moment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant