Django REST Framework: return 404 (not 400) on POS

2019-04-29 15:09发布

问题:

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.

回答1:

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.



回答2:

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'])