6

I'm developing a REST API which takes POST requests from some really brain-dead software which can't PATCH or anything else. The POSTs are to update Model objects which already exist in the database.

Specifically, I'm POSTing data for objects with a related field (a SlugRelatedField, as the POSTer knows the 'name' attribute but NOT the 'pk'). However, I need to return a 404 if the POSTer sends data where the 'name' returns nothing on the SlugRelatedField (e.g. the related object does not exist). I've been through this with a debugger but it seems that DRF uses some Django signals magic to do it The Way DRF Does It™, which is to return a 400 BAD REQUEST. I don't know how to modify this - only when it's the above condition and not a true 400-worthy POST - into a 404.

By the way, pre_save() in my view is NOT executing during execution of the failing test.

Here's the serializer:

class CharacterizationSerializer(serializers.ModelSerializer):
    """
    Work-in-progress for django-rest-framework use.  This handles (de)serialization
    of data into a Characterization object and vice versa.

    See: http://www.django-rest-framework.org/tutorial/1-serialization
    """
    creator = serializers.Field(source='owner.user.username')
    sample = serializers.SlugRelatedField(slug_field='name',
                                          required=True,
                                          many=False,
                                          read_only=False)

    class Meta:
        model = Characterization
        # leaving 'request' out because it's been decided to deprecate it. (...maybe?)
        fields = ('sample', 'date', 'creator', 'comments', 'star_volume', 'solvent_volume',
                  'solution_center', 'solution_var', 'solution_minimum', 'solution_min_stddev',
                  'solution_test_len',)

And here's the view where pre_save isn't being run in the given test (but does get run in some others):

class CharacterizationList(generics.ListCreateAPIView):
    queryset = Characterization.objects.all()
    serializer_class = CharacterizationSerializer
    permission_classes = (AnonPostAllowed,)   # @todo XXX hack for braindead POSTer

    def pre_save(self, obj):
        # user isn't sent as part of the serialized representation,
        # but is instead a property of the incoming request.
        if not self.request.user.is_authenticated():
            obj.owner = get_dummy_proxyuser()   # this is done for CharacterizationList so unauthed users can POST. @todo XXX hack
        else:
            obj.owner = ProxyUser.objects.get(pk=self.request.user.pk)

        # here, we're fed a string sample name, but we need to look up
        # the actual sample model.
        # @TODO: Are we failing properly if it doesn't exist?  Should
        # throw 404, not 400 or 5xx.
        # except, this code doesn't seem to be run directly when debugging.
        # a 400 is thrown; DRF must be bombing out before pre_save?
        obj.sample = Sample.objects.get(name=self.request.DATA['sample'])

And for good measure, here's the failing test:

def test_bad_post_single_missing_sample(self):
    url = reverse(self._POST_ONE_VIEW_NAME)

    my_sample_postdict = self.dummy_plqy_postdict.copy()
    my_sample_postdict["sample"] = "I_DONT_EXIST_LUL"
    response = self.rest_client.post(url, my_sample_postdict)
    self.assertTrue(response.status_code == 404,
                    "Expected 404 status code, got %d (%s). Content: %s" % (response.status_code, response.reason_phrase, response.content))

If I put a breakpoint in at the self.rest_client.post() call, the response already has a 400 in it at that point.

Gordon Morehouse
  • 115
  • 1
  • 2
  • 10

2 Answers2

8

You can use a Django Shortcut for that, getting the obj.sample:

from django.shortcuts import get_object_or_404
obj.sample = get_object_or_404(Sample, name=self.request.DATA['sample'])
fixmycode
  • 8,220
  • 2
  • 28
  • 43
  • Thanks - a friend suggested that to me, but it didn't work because pre_save() is not being run in this case. I may end up adding it in addition to other code to fix the problem though. Also, my original comment to this effect disappeared mysteriously. – Gordon Morehouse Jun 26 '14 at 17:26
  • Then you may want to address the issue of pre_save not running first, the most probable cause it's an exception in the parsing of the post data, maybe because of permissions. Try using AllowAny – fixmycode Jun 27 '14 at 17:31
  • I'll check into it - the view is set up so that aaaanybody in the world can POST, though, and response.content is complaining that the Sample with the given name doesn't exist - so if there's a permissions problem, it's not revealed yet. I'm just back to working with the code now - thanks for your hints. – Gordon Morehouse Jun 28 '14 at 01:09
6

Instead of using pre_save why not override post in your API view:

def post(self, request, *args, **kwargs):
    ...other stuff
    try:
        obj.sample = Sample.objects.get(name=self.request.DATA['sample'])
        ...or whatever other tests you want to do
    except:
        return Response(status=status.HTTP_404_NOT_FOUND)

    response = super(CharacterizationList, self).post(request, *args, **kwargs)
    return response

Make sure you import DRF's status:

from rest_framework import status

Also, note you will likely want to be more specific with the Exceptions you catch. Django's get method will return either DoesNotExist if nothing matches or MultipleObjectsReturned if more than one object matches. The relevant documentation:

Note that there is a difference between using get(), and using filter() with a slice of [0]. If there are no results that match the query, get() will raise a DoesNotExist exception. This exception is an attribute of the model class that the query is being performed on - so in the code above, if there is no Entry object with a primary key of 1, Django will raise Entry.DoesNotExist.

Similarly, Django will complain if more than one item matches the get() query. In this case, it will raise MultipleObjectsReturned, which again is an attribute of the model class itself.

Fiver
  • 9,909
  • 9
  • 43
  • 63
  • Thanks! Quick question - is get_object_or_404 inappropriate to use if overriding POST in this way? I'm still new to DRF and I'm a bit confused about when it expects Response objects or not. – Gordon Morehouse Jun 28 '14 at 01:11
  • Answered my own comment. I ended up omitting the try/except and using a dummy get_object_or_404 call in an overridden post() much as you specify, just to throw the 404 right away. I then used @fixmycode's suggestion below, too, for good measure. I wish I could accept both answers. All my tests pass now, thanks much. – Gordon Morehouse Jun 28 '14 at 01:18
  • @GordonMorehouse There's nothing wrong with using `get_object_or_404` I just like using DRF's status options as it keeps all my response status code consistent and it [provides nearly all the HTTP codes](http://www.django-rest-framework.org/api-guide/status-codes). I'll +1 @fixmycode's answer for ya! – Fiver Jun 28 '14 at 01:27
  • I'm having difficulties to find the module where `Response` appears. Any clues? – vabada Dec 22 '15 at 12:39
  • 1
    @dabad Try `from rest_framework.response import Response` – Fiver Dec 22 '15 at 14:34