I'm following the Django REST framework tutorial (part 4) but I'm observing a discrepancy between the behavior of my API and what is expected according to the tutorial. Here is my project-level urls.py:
from django.contrib import admin
from django.urls import path, include
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
path('admin/', admin.site.urls),
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
path('', include('snippets.urls'))
]
urlpatterns += [
path('api-auth/', include('rest_framework.urls'))
]
and here is the included snippets.urls:
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
path('snippets/', views.SnippetList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view())
]
urlpatterns = format_suffix_patterns(urlpatterns)
I've created a permissions.py with a custom permission class IsOwnerOrReadOnly:
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
and I've added this class to permission_classes in the SnippetDetail class in views.py:
from django.contrib.auth.models import User
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework import generics, permissions
from snippets.permissions import IsOwnerOrReadOnly
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,)
However, when I make a POST request specifying only my credentials (using Httpie), I get a 400 Bad Request:
Kurts-MacBook-Pro:~ kurtpeek$ http -a kurtpeek:mypassword POST http://localhost:8000/snippets/ code="print 789"
HTTP/1.1 400 Bad Request
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 37
Content-Type: application/json
Date: Wed, 24 Jan 2018 18:13:47 GMT
Server: WSGIServer/0.2 CPython/3.6.4
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN
{
"owner": [
"This field is required."
]
}
Apparently, I need to specify owner="1" to get this to work:
Kurts-MacBook-Pro:~ kurtpeek$ http -a kurtpeek:mypassword POST http://localhost:8000/snippets/ code="print 789" owner="1"
HTTP/1.1 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 103
Content-Type: application/json
Date: Wed, 24 Jan 2018 18:13:35 GMT
Server: WSGIServer/0.2 CPython/3.6.4
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN
{
"code": "print 789",
"id": 3,
"language": "python",
"linenos": false,
"owner": 1,
"style": "friendly",
"title": ""
}
However, as I understand from the logic of the has_object_permission method of the IsOwnerOrReadOnly permissions class, the owner should be inferred from the request.user. This is also not how the example is given in the tutorial. Can anyone spot what's wrong here?