How do I configure Tastypie to treat a field as unique?
Asked Answered
R

3

8

How do I configure Tastypie to treat a field as unique? My expectation would be to receive some sort of non-500 error (possibly a 409 conflict?) as a response if I try to insert duplicate entries for the field marked as unique.


I've looked through the docs and it looks like it should be obvious to me, but for some reason I'm not getting the response I would expect to see.

Here's the documentation link:

http://readthedocs.org/docs/django-tastypie/en/latest/fields.html?highlight=unique


The sample code is as follows:

urls.py

v1_api = Api(api_name='v1')
v1_api.register(CompanyResource())

urlpatterns = patterns('',
    (r'^api/', include(v1_api.urls)),
)

resource.py

class CompanyResource(ModelResource):

    CompanyName = fields.CharField(attribute='company_name')
    CompanyId = fields.CharField(attribute='company_id', unique=True)
    Contact = fields.CharField(attribute='contact')
    Email = fields.CharField(attribute='email')
    Phone = fields.CharField(attribute='phone')

    class Meta:
        queryset = Company.objects.all()
        authentication = BasicAuthentication()
        authorization = Authorization()
        allowed_methods = ['get', 'post']

models.py

class Company(models.Model):

    company_name = models.TextField(default=None, blank=True, null=True)
    company_id = models.CharField(default='', unique=True, db_index=True, max_length=20)
    contact = models.TextField(default=None, blank=True, null=True)
    email = models.EmailField(default=None, blank=True, null=True)
    phone = models.TextField(default=None, blank=True, null=True)

The error I receive is the following (using curl to hit my local service):

curl --dump-header - -H "Content-Type: application/json" -X POST --user user:password --data '{"CompanyName": "company", "CompanyId": "1234567890", "Contact": "John", "Email": "[email protected]", "Phone": "555-555-5555"}' http://localhost:8000/api/v1/company/
HTTP/1.0 500 INTERNAL SERVER ERROR
Date: Thu, 15 Sep 2011 18:25:20 GMT
Server: WSGIServer/0.1 Python/2.7.1
Content-Type: application/json; charset=utf-8

{"error_message": "(1062, \"Duplicate entry '1234567890' for key 'api_company_company_id_uniq'\")", 
...<snip>...
raise errorclass, errorvalue\n\nIntegrityError: (1062, \"Duplicate entry '1234567890' for key 'api_company_company_id_uniq'\")\n"}

When I remove unique=True, db_index=True, from the Company model, I don't receive the Integrity error, but instead, a new, duplicate resource is created. Again, this isn't the expected result as I would expect unique to preform some validation and cause some non-500 response.

Rabe answered 15/9, 2011 at 18:57 Comment(1)
The answer for me was to use validation=FormValidation(form_class=<MyModelForm>). This validates against your model's fields.Kabyle
R
8

Here is how I solved the problem:

Based on the documentation for validation, I was able to implement a custom validator that checked the uniqueness of the field for me. http://django-tastypie.readthedocs.org/en/latest/validation.html

In the CompanyResource, I added to the class meta a CustomValidation. I placed the implementation for CustomValidation in a validations.py file. If isValid returns errors, the api will return a 400 with the messages included in errors.

class CompanyResource(ModelResource):
    """
    CompanyIds should be unique
    """     
    CompanyName = fields.CharField(attribute='company_name')     
    CompanyId = fields.CharField(attribute='company_id', unique=True)     
    Contact = fields.CharField(attribute='contact')     
    Email = fields.CharField(attribute='email')     
    Phone = fields.CharField(attribute='phone')    

    class Meta:        
        queryset = Company.objects.all()        
        authentication = BasicAuthentication()        
        authorization = Authorization()        
        allowed_methods = ['get', 'post']                
        validation = CustomValidation()

validations.py

class CustomValidation(Validation):
    """
    The custom validation checks two things:
       1) that there is data
       2) that the CompanyId exists (unique check)
    """
    def is_valid(self, bundle, request=None):
        if not bundle.data:
            return {'__all__': 'Missing data, please include CompanyName, CompanyId, Contact, Email, and Phone.'}

        errors = {}                                    
        company_id=bundle.data.get('CompanyId', None)

        # manager method, returns true if the company exists, false otherwise
        if Company.objects.company_exists(company_id):
            errors['CompanyId']='Duplicate CompanyId, CompanyId %s already exists.' % company_id
        return errors
Rabe answered 21/9, 2011 at 10:14 Comment(1)
It doesn't. If you need PUT support you would need to go 'thoslins' approach above.Moriahmoriarty
N
5

I had the same problem today. Here is how I deal with it:

Override [request_method]_[request_type] method in your resource definition. For example, I override post_list in FooResource:

def post_list(self, request, **kwargs):
    from django.db import IntegrityError
    try:
        return super(FooResource, self).post_list(request, **kwargs)
    except IntegrityError, e:
        if e.args[0] == 1062:
            return http.HttpConflict()

Hope it works for you.

Narvaez answered 9/1, 2012 at 5:57 Comment(1)
Why did you choose to import IntegrityError inside the method?Hurry
H
0

For what it's worth, I've created a slightly different solution that works better for me. It's based on thoslin's answer.

I found that the e.args[0] == 1062 check fails for me. I'm pretty sure this is a MySQL error and I'm using Postgres.

I also implemented this in the obj_create method so that it will handle all object creation, not just that done via post_list.

from tastypie.exceptions import ImmediateHttpResponse
from tastypie.http import HttpConflict
from django.db import IntegrityError

...

class MyBaseResource(ModelResource):
    def obj_create(self, bundle, **kwargs):
        try:
            return super(MyBaseResource, self).obj_create(bundle, **kwargs)
        except IntegrityError, e:
            if e.args[0] == 1062 or e.args[0].startswith('duplicate key'):
                raise ImmediateHttpResponse(HttpConflict())

...

class MyResource(MyBaseResource):
    [usual resource stuff]
Hurry answered 9/4, 2014 at 3:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.