I am trying to write a REST API for my Django project using tastypie. I can use it to POST some data:

curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"name": "environment1", "last_active": "2015-06-18T15:56:37"}' http://localhost:8000/api/v1/report_status/

which works as it gets put into the database, but when I come to send a second set of data with the enviroment name (i.e. resend the same request), with the intention of replace the first set of data sent, I get the following error (abbreviated):

{"error_message": "duplicate key value violates unique constraint \"<project_name>_environment_name_key\"\nDETAIL:  Key (name)=(production) already exists.\n", "traceback": "Traceback ... django.db.utils.IntegrityError: duplicate key value violates unique constraint \"oilserver_environment_name_key\"\nDETAIL:  Key (name)=(production) already exists.

I understand that I have set the environment name to be unique, but I'm trying to replace the data, not upload another environment with the same name. The problem seems to be that the id is auto-incrementing. I do not wish to have to provide an id every time - I want the end user to simply supply an environment name and have it replace the data if it is already in the database. Can anyone tell me how this is usually done?

Below is the relevant parts of the code. I have a foreign key which I'm not sure whether or not complicates things, or if this is another matter entirely.

models.py:

from django.db import models


class EnvironmentState(models.Model):
    name = models.CharField(
        max_length=255,
        default="Unknown",
        help_text="Current state of the environment.")
    description = models.TextField(
        default=None,
        blank=True,
        null=True,
        help_text="Optional description for state.")

    def __str__(self):
        return self.name


class Environment(models.Model):
    name = models.CharField(
        max_length=255,
        unique=True,
        help_text="Name of environment")
    last_active = models.DateTimeField(
        default=None,
        blank=True,
        null=True,
        help_text="DateTime when environment message was last received.")
    current_situation = models.TextField(
        help_text="Statement(s) giving background to the current env status.")
    status = models.ForeignKey(EnvironmentState)

    def __str__(self):
        return self.name

resources.py:

from tastypie import fields
from tastypie.resources import ModelResource
from tastypie.authorization import Authorization
from oilserver.models import Environment, EnvironmentState
from oilserver.status_checker import StatusChecker


class EnvironmentStateResource(ModelResource):
    class Meta:
        queryset = EnvironmentState.objects.all()
        resource_name = 'environment_state'
        authorization = Authorization()


class ReportStatusResource(ModelResource):
    status = fields.ForeignKey(EnvironmentStateResource, 'status', 
                               null=True, full=True)

    class Meta:
        queryset = Environment.objects.all()
        resource_name = 'report_status'
        authorization = Authorization()

    def hydrate(self, bundle):
        name = bundle.data.get('name')
        last_active = bundle.data.get('last_active')

        status_checker = StatusChecker(last_active)
        # StatusChecker is just a class that takes in some data and 
        # generates a 'state' (up, down) and a 'situation' string explaining 
        # to the user what is going on.

        bundle.data['current_situation'] = status_checker.situation
        env_state = EnvironmentState.objects.get(name=status_checker.state)
        bundle.data['status'] = {"pk": env_state.pk}

        return bundle

So, where am I going wrong?

Thanks

3

There are 3 answers

1
Yahya Yahyaoui On BEST ANSWER

You have to target the single resource e.g:

http://localhost:8000/api/v1/report_status/<IDENTIFIER OF THE RESOURCE YOU WANT TO UPDATE>

And I think that you need a "PUT" request instead of "POST"

So after you create the environment, you get it's id, you send a "PUT" request to "http://localhost:8000/api/v1/report_status/< ID > and then it should work.

0
Darren On

Okay, so I ended up including a name check followed by bundle.data['id'] = env.id if it was already there:

class ReportStatusResource(ModelResource):
    status = fields.ForeignKey(EnvironmentStateResource, 'status', 
                               null=True, full=True)

    class Meta:
        queryset = Environment.objects.all()
        resource_name = 'report_status'
        authorization = Authorization()

    def hydrate(self, bundle):
        name = bundle.data.get('name')

        for env in Environment.objects.all():
            if env.name == name:
                bundle.data['id'] = env.id

        last_active = bundle.data.get('last_active')

        status_checker = StatusChecker(last_active)
        # StatusChecker is just a class that takes in some data and 
        # generates a 'state' (up, down) and a 'situation' string explaining 
        # to the user what is going on.

        bundle.data['current_situation'] = status_checker.situation
        env_state = EnvironmentState.objects.get(name=status_checker.state)
        bundle.data['status'] = {"pk": env_state.pk}

I'm open to other solutions though if anyone else has any better ideas...

0
Railslide On

You could expose environment name as an endpoint, so that your resource url will become http://localhost:8000/api/v1/report_status/[environment_name] and then use a PUT call, which will either update or create a new resource depending on whether the resource for the given environment name exists or not.