Search code examples
pythondjangotastypie

TastyPie: How to POST to an intermediate ManyToMany 'through' Resource


I have 2 Models (RiderProfile and Ride) that have a many-to-many relationship that is managed via an Intermediate Model (RideMembership).

I would like to be able to POST new relationship entries to my Intermediate ModelResource, but I am getting an error telling me that I am providing an invalid URL for my resource.

Note There is another question similar to this, but it does not provide the POST data.

Here are my Models:

class RiderProfile(models.Model):
    user = models.OneToOneField(User)
    age = models.IntegerField(max_length=2, null=True, blank=True)
    rides = models.ManyToManyField('riderapp.Ride', through="RideMembership", null=True, blank=True)

    def __unicode__(self):
        return self.user.get_username()

class Ride(models.Model):
    name = models.CharField(max_length=64)
    riders = models.ManyToManyField(RiderProfile, through="RideMembership", null=True, blank=True)

    def __unicode__(self):
        return self.name

class RideMembership(models.Model):
    rider = models.ForeignKey(RiderProfile)
    ride = models.ForeignKey(Ride)
    date_joined = models.DateField(default=datetime.now)
    invite_reason = models.CharField(max_length=64)

    def __unicode__(self):
        return self.rider.user.get_username() + ' to ' + self.ride.name()

And here are my resources:

class UserResource(ModelResource):
    ...

class RiderProfileResource(ModelResource):
    class Meta:
        queryset = RiderProfile.objects.all()
        resource_name = 'riders'

    user = fields.ForeignKey(UserResource, 'user', full=True)
    rides = fields.ToManyField('riderapp.api.RideForRiderProfileResource', 'rides', full=True)

class RideResource(ModelResource):
    class Meta:
        queryset = Ride.objects.all()
        resource_name = 'rides'
        authorization = Authorization()
        always_return_data = True

    riders = fields.ToManyField('riderapp.api.RiderProfileForRideResource', 'riders', full=True, blank=True, readonly=True)

class RideMembershipResource(ModelResource):
    class Meta:
        queryset = RideMembership.objects.all()
        resource_name = 'rider_memberships'
        authorization = Authorization()
        always_return_data = True

    rider = fields.ForeignKey(RideForRiderProfileResource, 'rider')
    ride = fields.ForeignKey(RideResource, 'ride')

"""special ride resource for inclusion in a RiderProfile. Omits the `riders` relational field to avoid infinite recursion"""
class RideForRiderProfileResource(ModelResource):
    class Meta:
        queryset = Ride.objects.all()
        resource_name = 'rides_for_riders'
        allowed_methods = [];

""" special rider profile resource for inclusion in a Ride. Omits the `rides` relational field to avoid infinite recursion """
class RiderProfileForRideResource(ModelResource):
    class Meta:
        queryset = RiderProfile.objects.all()
        resource_name = 'riders_for_ride'
        allowed_methods = [];

    user = fields.ForeignKey(UserResource, 'user', full=True)

Here is how I am trying to create a new RideMemebership relationship via POST:

URL: http://[...]/api/v1/rider_memberships/

Data:

{
    "rider": "/api/v1/riders/1/",
    "ride": "/api/v1/rides/12/",
    "invite_reason": "because it's my ride!"
}

Response:

{
    "error_message": "An incorrect URL was provided '/api/v1/riders/1/' for the 'RideForRiderProfileResource' resource.",
    "traceback": "Traceback (most recent call last):

  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 201, in wrapper
    response = callback(request, *args, **kwargs)

  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 432, in dispatch_list
    return self.dispatch('list', request, **kwargs)

  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 464, in dispatch
    response = method(request, **kwargs)

  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 1340, in post_list
    updated_bundle = self.obj_create(bundle, **self.remove_api_resource_names(kwargs))

  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 2103, in obj_create
    bundle = self.full_hydrate(bundle)

  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 896, in full_hydrate
    value = field_object.hydrate(bundle)

  File \"/Library/Python/2.7/site-packages/tastypie/fields.py\", line 746, in hydrate
    return self.build_related_resource(value, request=bundle.request)

  File \"/Library/Python/2.7/site-packages/tastypie/fields.py\", line 659, in build_related_resource
    return self.resource_from_uri(self.fk_resource, value, **kwargs)

  File \"/Library/Python/2.7/site-packages/tastypie/fields.py\", line 578, in resource_from_uri
    obj = fk_resource.get_via_uri(uri, request=request)

  File \"/Library/Python/2.7/site-packages/tastypie/resources.py\", line 810, in get_via_uri
    raise NotFound(\"An incorrect URL was provided '%s' for the '%s' resource.\" % (uri, self.__class__.__name__))

NotFound: An incorrect URL was provided '/api/v1/riders/1/' for the 'RideForRiderProfileResource' resource.
"
}

Both of the resource URIs in my POST data point to valid resources, so I am not sure what is causing this to fail. I am testing with the Postman extension in Chrome, if that matters. I am able to POST new Rides, but just not RideMembership entries.

Thanks guys.

Update

As ozgur pointed out (see accepted answer), I was referencing the wrong the resources in my RideMembershipResource. I have multiple ModelResources for my Ride and RiderProfile models so I can include limited versions of each on both sides of my many-to-many relationships. I updated my RideMembershipResource to point to the proper ForgeignKey resources like so:

class RideMembershipResource(ModelResource):
    class Meta:
        queryset = RideMembership.objects.all()
        resource_name = 'rider_memberships'
        authorization = Authorization()
        always_return_data = True

    rider = fields.ForeignKey('riderapp.api.RiderProfileForRideResource', 'rider')
    ride = fields.ForeignKey('riderapp.api.RideForRiderProfileResource', 'ride')

And changed my post data to:

{
    "rider": "/api/v1/riders_for_ride/1/",
    "ride": "/api/v1/rides_for_riders/12/",
    "invite_reason": "because it's my ride!"
}

Now I can POST relationships. Cheers!


Solution

  • The resource RideMembershipResource are joining two separate Ride instances.

    I think you should change rider field to the following in your RideMembershipResource resource;

    rider = fields.ForeignKey(RiderProfileForRideResource, 'rider')
    

    I don't have much knowledge about tastypie but you may need to change the resource url in your POST data to this:

    {
        "rider": "/api/v1/riders_for_ride/1/",
        "ride": "/api/v1/rides/12/",
        "invite_reason": "because it's my ride!"
    }