djangotastypie

Saving M2M field with Tastypie


I'm building an API with Tastypie, and I've run into an issue when saving a many-to-many field.

I have a model call Pest and another called Call, and Call has a field called pests representing the pests that can be applied to a call. These already exist and the user can choose one or more to apply to that call - there is no intention to create them at the same time as the Call object.

By default, I get the following error when I try to create a new Call via POST:

{"error_message": "Cannot resolve keyword 'url' into field. Choices are: baitpoint, call, description, id, name, operator", "traceback": "Traceback (most recent call last):\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 217, in wrapper\n    response = callback(request, *args, **kwargs)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 459, in dispatch_list\n    return self.dispatch('list', request, **kwargs)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 491, in dispatch\n    response = method(request, **kwargs)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 1357, in post_list\n    updated_bundle = self.obj_create(bundle, **self.remove_api_resource_names(kwargs))\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 2150, in obj_create\n    return self.save(bundle)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 2300, in save\n    m2m_bundle = self.hydrate_m2m(bundle)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 964, in hydrate_m2m\n    bundle.data[field_name] = field_object.hydrate_m2m(bundle)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/fields.py\", line 853, in hydrate_m2m\n    m2m_hydrated.append(self.build_related_resource(value, **kwargs))\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/fields.py\", line 653, in build_related_resource\n    return self.resource_from_uri(self.fk_resource, value, **kwargs)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/fields.py\", line 573, in resource_from_uri\n    obj = fk_resource.get_via_uri(uri, request=request)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 838, in get_via_uri\n    return self.obj_get(bundle=bundle, **self.remove_api_resource_names(kwargs))\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/tastypie/resources.py\", line 2125, in obj_get\n    object_list = self.get_object_list(bundle.request).filter(**kwargs)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/django/db/models/query.py\", line 655, in filter\n    return self._filter_or_exclude(False, *args, **kwargs)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/django/db/models/query.py\", line 673, in _filter_or_exclude\n    clone.query.add_q(Q(*args, **kwargs))\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py\", line 1266, in add_q\n    can_reuse=used_aliases, force_having=force_having)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py\", line 1134, in add_filter\n    process_extras=process_extras)\n\n  File \"/home/matthew/Projects/Pestability/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py\", line 1332, in setup_joins\n    \"Choices are: %s\" % (name, \", \".join(names)))\n\nFieldError: Cannot resolve keyword 'url' into field. Choices are: baitpoint, call, description, id, name, operator\n"}

So I had a look and found this answer, which seemed to cover a similar situation. I added the hydrate_pests method to the CallResource class as follows:

class AbstractModelResource(ModelResource):
    class Meta:
        authorization = DjangoAuthorization()
        authentication = ApiKeyAuthentication()
        cache = SimpleCache(timeout=10)
        always_return_data = True


class FilteredByOperatorAbstractModelResource(AbstractModelResource):
    def authorized_read_list(self, object_list, bundle):
        user = bundle.request.user
        site_user = SiteUser.objects.get(user=user)
        return object_list.filter(operator=site_user.operator)


class PestResource(FilteredByOperatorAbstractModelResource):
    class Meta(AbstractModelResource.Meta):
        queryset = Pest.objects.all()
        resource_name = 'pest'
        allowed_methods = ['get']


class CallResource(AbstractModelResource):
    client = fields.ForeignKey(ClientResource, 'client')
    operator = fields.ForeignKey(OperatorResource, 'operator')
    pests = fields.ManyToManyField(PestResource, 'pests', null=True)

    class Meta(AbstractModelResource.Meta):
        queryset = Call.objects.all()
        resource_name = 'call'

    def hydrate_pests(self, bundle):
        pests =  bundle.data.get('pests', [])
        pest_ids = []
        for pest in pests:
            m = re.search('\/api\/v1\/pests\/(\d+)\/', str(pest))
            try:
                id = m.group(1)
                pest_ids.append(id)
            except AttributeError:
                pass

        bundle.data['pests'] = Pest.objects.filter(id__in=pest_ids)
        return bundle

The pests field is getting passed through as follows:

0: "/api/v1/pests/6/"
1: "/api/v1/pests/7/"

And the pest URL's are showing up correctly when I run bundle.data.get('pests', []) - if I use PDB to set a trace, I can verify that the URLs are being passed through, and Pest.objects.filter(id__in=pest_ids) is returning the correct items. However, although the HTTP POST request is successful, the pests field is not being updated to reflect the new data.

Can anyone see where I've gone wrong? Am I correct in passing through a list of the Pest objects to bundle.data['pests'], or is this not how I should be passing this data through to that field?

What actually gets passed through to bundle.data is as follows:

{'pests': [<Pest: Rats>, <Pest: Mice>], 'notes': u'Blah', 'first_choice_visit_time': u'2013-07-18T02:02', 'client': u'/api/v1/client/28/', 'date': u'2013-07-18', 'second_choice_visit_time': u'2014-03-03T03:02'}

Solution

  • The bundle data holds dictionaries. You're passing it a list of QuerySet objects. Try appending .values() to your queryset.