Search code examples
tastypie

How to make POST get_or_create related Resources in tastypie


I'm using Tastypie to create a REST API for a Django app and want to be able to create new objects and related objects in one POST. The related objects are specified by a name used to look them up, and I want to create new ones if the name is not found.

Given Django models like this:

class Product(Model):
    name = CharField(max_length=32)

class Release(Model):
    product = ForeignKey(to=Product, related_name='releases')
    version = CharField(max_length=32)

And these Tastypie resources:

class ProductResource(ModelResource):
    class Meta:
        queryset = Product.objects.all()
        resource_name = 'product'

class ReleaseResource(ModelResource):
    class Meta:
        queryset = Release.objects.all()
        resource_name = 'release'

    def hydrate_product(self, bundle):
        """Replace product name with id of existing/created one."""
        product = Product.objects.get_or_create(name=bundle.data['product'])
        bundle.data['product'] = product.id
        return bundle

And an empty database

When I POST this data to my tastypie REST API:

POST /api/release {
    "product": "Cool Widget",
    "version": "1.2.3",
}

Then I want these model objects to be created:

product = Product(name="Cool Widget")
release = Release(product=product, version="1.2.3")

But I get an exception something like this:

IntegrityError: null value in column "product_id" violates not-null constraint
DETAIL:  Failing row contains (1, null, 1.2.3).

And the hydrate_product() method is not called.

When I add this class property to ReleaseResource:

product = fields.ToOneField(ProductResource, 'product')

Then I get something like this:

NotFound: An incorrect URL was provided 'Cool Widget' for the 'ProductResource' resource.

How do I replace the product name in the bundle with the URI of the created/existing Product object having that name?


Solution

  • I think my mistake is in trying to modify the purpose of relation fields. They should be left to work normally if my resource actually includes them.

    I should just declare ReleaseResource.product as a CharField and implement Release.obj_create to use Product.objects.get_or_create for the related Product object when creating the new Release.

    class ReleaseResource(ModelResource):
        product = CharField(attribute='product__name')
    
        class Meta:
            queryset = Release.objects.all()
            resource_name = 'release'
    
        def obj_create(self, bundle, **kwargs)
            product = Product.objects.get_or_create(name=bundle.data['product'])[0]
            super(ModelResource, self).obj_create(bundle, product=product, **kwargs)
    

    It doesn't seem clear enough in the doco that no relation fields are populated on a ModelResource automatically, you have to explicitly declare all relation fields, not just reverse ones.