Django Tastypie: Imlementing Many To Many "through" relationships
Asked Answered
K

1

6

I have searched about this issue a lot and gone through a bunch of related questions on Stack Overflow, but there doesn't seem to be a definitive answer about how to implement many-to-many relationships "through" an intermediate model (or maybe I missed it).

I have a model named Sample which has a many-to-many relationship with Region. There is an intermediate model which connects the two, named SampleRegion. I am currently not saving any extra information on the intermediate model, but I might in the future.

Here are my models:

class Sample(models.Model):
    sample_id = models.BigIntegerField(primary_key=True)
    description = models.TextField(blank=True)
    objects = models.GeoManager()
    regions = ManyToManyField(Region, through='SampleRegion')
    class Meta:
        db_table = u'samples'
    def save(self, **kwargs):
        # Assign a sample ID only for create requests
        if self.sample_id is None:
            try: id = Sample.objects.latest('sample_id').sample_id + 1
            except Sample.DoesNotExist: id = 1
            self.sample_id = id
        super(Sample, self).save

class Region(models.Model):
    name = models.CharField(max_length=100, unique=True)
    def __unicode__(self):
        return self.name
    class Meta:
        db_table = u'regions'

class SampleRegion(models.Model):
    sample = models.ForeignKey('Sample')
    region = models.ForeignKey(Region)
    class Meta:
        unique_together = (('sample', 'region'),)
        db_table = u'sample_regions'

And here is one approach I took to write the resources. It's not correct, and I am not able to figure out the right way of doing it:

class SampleResource(ModelResource):
    regions = fields.ToManyField("tastyapi.resources.RegionResource",
                                  "regions")
    class Meta:
        queryset = models.Sample.objects.all()
        allowed_methods = ['get', 'post', 'put', 'delete']
        authentication = ApiKeyAuthentication()
        authorization = ObjectAuthorization('tastyapi', 'sample')
        excludes = ['user', 'collector']
        filtering = {
                'version': ALL,
                'sesar_number': ALL
                }
        validation = VersionValidation(queryset, 'sample_id')

    def hydrate_regions(self, bundle): 
        # code to create a new SampleRegion object by getting a list of 
        # regions from bundle.data['regions']

class RegionResource(ModelResource):
    class Meta:
        queryset = models.Region.objects.all()
        allowed_methods = ['get']
        resource_name = "region"
        filtering = {
                'region': ALL,
                }

This is how I making a POST request:

post_data = {
    'regions': ["/tastyapi/v1/region/2/"],
    'description': 'Created by a test case',
}

client.post('/tastyapi/v1/sample/', data = post_data,
            authentication = credentials, format = 'json')

This request doesn't work because bundle.data['regions'] is None by the time it reaches hydrate_regions.

Does anybody have any advice on how I should go about implementing this scenario?

Kyoko answered 24/3, 2014 at 4:40 Comment(0)
K
9

I figured this out a couple of days ago. Here's what I found...

Django takes care of creating M2M relationships for you if you are not explicitly creating an intermediate table. However, if you are explicitly using an intermediate table, then you are responsible for creating a record in the intermediate table. To get this working in Tastypie, I had to override the save_m2m method to explicitly create a record in the intermediate table linking my the sample I have just created and an existing region.

This is how the relevant part of my resources.py looks like now:

class SampleResource(ModelResource):
    regions = fields.ToManyField("tastyapi.resources.RegionResource",
                                 "regions")

    class Meta:
        queryset = models.Sample.objects.all()
        allowed_methods = ['get', 'post', 'put', 'delete']
        authentication = ApiKeyAuthentication()
        authorization = ObjectAuthorization('tastyapi', 'sample')
        excludes = ['user', 'collector']
        filtering = {
                'regions': ALL_WITH_RELATIONS,
                }
        validation = VersionValidation(queryset, 'sample_id')

    def save_m2m(self, bundle):
        for field_name, field_object in self.fields.items():
            if not getattr(field_object, 'is_m2m', False):
                continue

            if not field_object.attribute:
                continue

            for field in bundle.data[field_name]:
                kwargs = {'sample': models.Sample.objects.get(pk=bundle.obj.sample_id),
                          'region': field.obj}

                try: SampleRegion.objects.get_or_create(**kwargs)
                except IntegrityError: continue

class RegionResource(BaseResource):
    class Meta:
        queryset = models.Region.objects.all()
        authentication = ApiKeyAuthentication()
        allowed_methods = ['get']
        resource_name = "region"
        filtering = { 'region': ALL }
Kyoko answered 27/3, 2014 at 20:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.