Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Posting a one-to-many relationship

I'm trying to expose an API to my Django model through Django REST framework.

I have an object Observation. An observation can contain multiple things that have been observed. So I represented it like this:

class Observation(models.Model):

    photo_file = models.ImageField( upload_to=img_dir,   blank=True, null=True )
    titestamp = models.DateTimeField(blank=True, null=True)
    latitude = models.FloatField()
    longitude = models.FloatField()


class ObservedThing(models.Model):
    thing = models.ForeignKey(Thing) # the thing being observed
    observation = models.ForeignKey(Observation, related_name='observed_thing')
    value = models.FloatField()

As I understand this is a one-to-many relationship.

I now have an API View:

class ObsvList(generics.ListCreateAPIView):
    """
    API endpoint that represents a list of observations.
    """
    model = Observation
    serializer_class = ObsvSerializer

and the corresponding serialiser:

class ObsvSerializer(serializers.ModelSerializer):

    observed_thing = serializers.PrimaryKeyRelatedField(many=True)

    class Meta:
        model = Observation

What do I have to do to be able to POST an observation with several things detected? I cannot figure it out. Many thanks.

like image 672
gozzilli Avatar asked Mar 04 '13 13:03

gozzilli


2 Answers

(answer more or less copied from another similar but less clear question)

To create multiple related objects in a single POST requires writable nested serializers which are not yet available.

Full support is a work in progress, but in the mean time one (hacky) solution is to override the create method in the view in each case:

class FooListCreateView(ListCreateAPIView):
    model = Foo
    serializer_class = FooSerializer

    def create(self, request, *args, **kwargs):
        data=request.DATA

        f = Foo.objects.create()

        # ... create nested objects from request data ...  

        # ...
        return Response(serializer.data, 
                        status=status.HTTP_201_CREATED,
                        headers=headers)

Probably not ideal, but it works for me until the proper way comes along.

The other option is to create the related Observation objects individually with separate POSTs, and the use PrimaryKeyRelatedField or HyperlinkedRelatedField to make the associations in the final ObservedThing POST.

like image 162
Rob Agar Avatar answered Sep 19 '22 18:09

Rob Agar


I know this thread has already an answer but I started working to solve this problem, and since this post was one of my inspirations, I would like to share my final solution. It can be useful to someone. I have the models, so the parent class:

#parent model class
class Parent(models.Model):

    id = models.AutoField(primary_key=True)
    field = models.CharField(max_length=45)

    class Meta:
        managed = False
        db_table = 'parent'

then, the child class:

#child model class
class Child(models.Model):

    id = models.AutoField(primary_key=True)
    field = models.CharField(max_length=45)
    parent = models.ForeignKey(Parent, related_name='children')

    class Meta:
        managed = False
        db_table = 'child'

I had to define the serializers, since I didn't want to create a router accessible url to directly manage Children objects, but I wanted to create them through the ModelViewSet of the parent ModelViewSet, this is what I needed:

class ChildSerializer(serializers.ModelSerializer):
    class Meta:
        model = Child
        read_only_fields = ('id',)

class ParentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Banner
        read_only_fields = ('id',)

class ParentSerializerNested(ParentSerializer):
    children = ChildSerializer(many=True)

I was then ready to create the ModelViewSet, overriding/extending the create/update mixins, and make it generic in order to reuse it for other cases:

class ParentChildViewSet(viewsets.ModelViewSet):

    def create(self, request, *args, **kwargs):
        serializer = self.serializer_parent(data=request.DATA,
                                            files=request.FILES)

        try:
            if serializer.is_valid():
                with transaction.commit_on_success():
                    self.pre_save(serializer.object)
                    parent = serializer.save(force_insert=True)
                    self.post_save(parent, created=True)

                    # need to insert children records
                    for child in request.DATA[self.child_field]:
                        child[self.parent_field] = parent.id
                        child_record = self.serializer_child(data=child)
                        if child_record.is_valid():
                            child_record.save(force_insert=True)
                        else:
                            raise ValidationError('Child validation failed')

                    headers = self.get_success_headers(serializer.data)

                    serializer.data[self.child_field] = self.serializer_child(
                        self.model_child.objects.filter(
                            **{self.parent_field: parent.id}).all(),
                            many=True).data
                    return Response(serializer.data,
                                    status=status.HTTP_201_CREATED,
                                    headers=headers)
        except ValidationError:
            pass
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

So I can reuse it for every nested relationship case I have in my app like this:

class ParentViewSet(ParentChildViewSet):
    child_field = 'children'
    parent_field = 'parent'
    model = Parent
    model_child = Child
    serializer_class = ParentSerializerNested
    serializer_parent = ParentSerializer
    serializer_child = ChildSerializer

And in the end, the routing:

router = routers.DefaultRouter()
router.register(r'parents', ParentViewSet)

It works like a charm!

like image 39
gigaDIE Avatar answered Sep 18 '22 18:09

gigaDIE