beda-software / drf-writable-nested

Writable nested model serializer for Django REST Framework
Other
1.07k stars 116 forks source link

Error when trying to update reverse many-to-many relation #165

Closed lvonlanthen closed 2 years ago

lvonlanthen commented 2 years ago

Hi. I stumbled upon an error today and I am not sure whether it is a bug or if I am missing something.

I have two models with a many-to-many relation between them:

class Movie(models.Model):
    name = models.TextField()

class Actor(models.Model):
    name = models.TextField()
    movies = models.ManyToManyField(Movie, related_name="actors")

I'd like to create and update these entities from both sides so I have the following serializers:

class ActorNestedSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Actor
        fields = ["pk", "name"]

class MovieNestedSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Movie
        fields = ["pk", "name"]

class ActorSerializer(WritableNestedModelSerializer):
    movies = MovieNestedSerializer(many=True)

    class Meta:
        model = models.Actor
        fields = ["pk", "name", "movies"]

class MovieSerializer(WritableNestedModelSerializer):
    actors = ActorNestedSerializer(many=True)

    class Meta:
        model = models.Movie
        fields = ["pk", "name", "actors"]

When I create or update an Actor with Movies, everything works fine:

actor_data = {
    "name": "Jason Statham",
    "movies": [
        {"name": "Snatch"},
    ],
}
actor_serializer = ActorSerializer(data=actor_data)
actor_serializer.is_valid(raise_exception=True)
actor = actor_serializer.save()
assert ["Snatch"] == list(actor.movies.values_list("name", flat=True))

movie = Movie.objects.get()
update_actor_data = {
    "name": "Jason Statham",
    "movies": [
        {"pk": movie.pk, "name": "Snatch"},
        {"name": "The Transporter"},
    ],
}
actor_update_serializer = ActorSerializer(instance=actor, data=update_actor_data)
actor_update_serializer.is_valid(raise_exception=True)
actor = actor_update_serializer.save()
assert ["Snatch", "The Transporter"] == list(actor.movies.values_list("name", flat=True))

When trying to do the same but in reverse direction (create or update a Movie with Actors), it raises an Exception when trying to update the Movie:

movie_data = {
    "name": "Snatch",
    "actors": [
        {"name": "Jason Statham"},
    ],
}
movie_serializer = MovieSerializer(data=movie_data)
movie_serializer.is_valid(raise_exception=True)
movie = movie_serializer.save()
assert ["Jason Statham"] == list(movie.actors.values_list("name", flat=True))

actor = Actor.objects.get()
update_movie_data = {
    "name": "Snatch",
    "actors": [
        {"pk": actor.pk, "name": "Jason Statham"},
        {"name": "Brad Pitt"},
    ],
}
movie_update_serializer = MovieSerializer(instance=movie, data=update_movie_data)
movie_update_serializer.is_valid(raise_exception=True)
# XXX: The following line is throwing an error:
movie = movie_update_serializer.save()
assert ["Jason Statham", "Brad Pitt"] == list(movie.actors.values_list("name", flat=True))

It raises the following error: django.core.exceptions.FieldError: Cannot resolve keyword 'actors' into field. Choices are: id, movies, name

Stacktrace:

../path/to/virtualenv/lib/python3.9/site-packages/drf_writable_nested/mixins.py:232: in save
    return super(BaseNestedModelSerializer, self).save(**kwargs)
../path/to/virtualenv/lib/python3.9/site-packages/rest_framework/serializers.py:200: in save
    self.instance = self.update(self.instance, validated_data)
../path/to/virtualenv/lib/python3.9/site-packages/drf_writable_nested/mixins.py:290: in update
    self.delete_reverse_relations_if_need(instance, reverse_relations)
../path/to/virtualenv/lib/python3.9/site-packages/drf_writable_nested/mixins.py:336: in delete_reverse_relations_if_need
    model_class.objects.filter(
../path/to/virtualenv/lib/python3.9/site-packages/django/db/models/manager.py:85: in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
../path/to/virtualenv/lib/python3.9/site-packages/django/db/models/query.py:941: in filter
    return self._filter_or_exclude(False, args, kwargs)
../path/to/virtualenv/lib/python3.9/site-packages/django/db/models/query.py:961: in _filter_or_exclude
    clone._filter_or_exclude_inplace(negate, args, kwargs)
../path/to/virtualenv/lib/python3.9/site-packages/django/db/models/query.py:968: in _filter_or_exclude_inplace
    self._query.add_q(Q(*args, **kwargs))
../path/to/virtualenv/lib/python3.9/site-packages/django/db/models/sql/query.py:1396: in add_q
    clause, _ = self._add_q(q_object, self.used_aliases)
../path/to/virtualenv/lib/python3.9/site-packages/django/db/models/sql/query.py:1415: in _add_q
    child_clause, needed_inner = self.build_filter(
../path/to/virtualenv/lib/python3.9/site-packages/django/db/models/sql/query.py:1289: in build_filter
    lookups, parts, reffed_expression = self.solve_lookup_type(arg)
../path/to/virtualenv/lib/python3.9/site-packages/django/db/models/sql/query.py:1115: in solve_lookup_type
    _, field, _, lookup_parts = self.names_to_path(lookup_splitted, self.get_meta())

Any help is much appreciated, thank you!