I tried to add a Patch method to my API in Django and I'm always ending with a "Method not allowed".
I added mixins.UpdateModelMixin as mention in the Django Rest Framework documentation, however, it still returns the same error. I look and don't find where I need to put authorization for Patch to be allowed.
this is the code related to that view and path declaration in urls.py and views.py.
urls.py
schema_view = get_schema_view(
openapi.Info(
title="WAF Management Portal API",
default_version="v1",
description="REST api for interaction between Frontend and Backend.",
contact=openapi.Contact(email="soc-dev-automation#bell.ca"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
path(
'action/dothis/', ActionApiView.as_view(), name="action_api_view"
),
views.py
class ActionApiView(mixins.UpdateModelMixin, ActionAPIView):
"""
post:
add one or more settings to selected policy
patch:
modify or more settings to selected policy
"""
def get_queryset(self):
return Policy.objects.allowed_to_user(self.request.user)
def get_serializer(self, *args, **kwargs):
return SettingsSerializer(*args, **kwargs)
#swagger_auto_schema()
def post(self, request):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
selected_policies = serializer.get_selected_policies(queryset)
.....do some data manipulation (included action_id variable)...
response = {
....prepare response
}
return redirect("another_view", action_id=action_id)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#swagger_auto_schema()
def patch(self, request):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
selected_policies = serializer.get_selected_policies(queryset)
.....do some data manipulation (included action_id variable)...
response = {
....prepare response
}
return redirect("another_view", action_id=action_id)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
If ActionAPIView is inheriting from ModelViewSet , you may need to rename the patch function to update and change your urls to
path(
'action/dothis/', ActionApiView.as_view({'patch':'update'}), name="action_api_view"
),
Related
I'm testing whether an API User can POST to a view if the User can pass the IsAuthenticated permission. Upon running the test I get the assertion error: AssertionError: 401 != 201.
When permission_classes = (permissions.IsAuthenticated,) is commented out and the test is ran again, I'm finding that request.user is turning out to be an AnonymousUser. Yet, I have a User created and attached to the request as shown below.
I'm not sure what is causing this, and looking to understand how I can pass a User instance to the request.
Note - I'm trying to do this in the Django testing API rather than use Django REST Framework.
tests.py
class TestUserPreferencesResource(TestCase):
'''Verify that a User is capable of setting preferences for their profile'''
#classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user('Mock', password="secret")
cls.user_prefs = json.dumps({
"age": ['Baby', 'Adult'],
"gender": ['Male'],
"size": ["Medium", "Large"]
})
cls.factory = RequestFactory()
cls.credentials = b64encode(b"Mock:secret").decode("ascii")
def test_user_preferences_settings(self):
request = self.factory.post(
reverse("pref-settings"),
data=self.user_prefs,
content_type="application/json",
headers = {
"Authorization": f"Basic {self.credentials}"
},
)
request.user = self.user
print(request.user)
response = UserPrefView.as_view()(request)
self.assertEqual(response.status_code, 201)
views.py
class UserPrefView(
CreateModelMixin,
UpdateModelMixin,
GenericAPIView):
queryset = UserPref.objects.all()
permission_classes = (permissions.IsAuthenticated,)
serilaizer_class = serializers.UserPreferenceSerializer
def post(self, request, format=None):
import pdb; pdb.set_trace()
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(user=request.user)
return Response(serializer.data)
I have many endpoints which use ModelViewSet to manage CRUD operations for my models.
What I am trying to do, is to add bulk create, update, and delete at these same endpoints. In other words, I want to add POST, PUT, PATCH and DELETE to the collection endpoint (e.g.: /api/v1/my-model). There is a django-rest-framework-bulk package available, but it seems to be abandoned (hasn't been updated in 4 years) and I'm not comfortable using a package in production that is no longer active.
Additionally, There are several similar questions here that have solutions, as well as blog posts I've found. However, they all seem to use the base ViewSet, or APIView, which would require re-writing all of my existing ModelViewSet code.
Finally, there is the option of using the #action decorator, however this would require me to have a separate list endpoint (e.g.- /api/v1/my-model/bulk) which I'd like to avoid.
Are there any other ways to accomplish this while keeping my existing ModelViewSet views? I've been looking at GenericViewSet and mixins, and am wondering if creating my own mixin might be the way to go. However, looking at the mixin code, it doesn't appear that you can specify an HTTP Request method to be attached to a given mixin.
Finally, I have tried creating a separate ViewSet that accepts PUT and adding it to my URLs, but this doesn't work (I get a 405 Method not allowed when I try to PUT to /api/v1/my-model). The code I tried looks like this:
# views.py
class MyModelViewSet(viewsets.ModelViewSet):
serializer_class = MyModelSerializer
permission_classes = (IsAuthenticated,)
queryset = MyModel.objects.all()
paginator = None
class ListMyModelView(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def put(self, request):
# Code for updating list of models will go here.
return Response({'test': 'list put!'})
# urls.py
router = DefaultRouter(trailing_slash=False)
router.register(r'my-model', MyModelViewSet)
router.register(r'my-model', ListMyModelView, base_name='list-my-model')
urlpatterns = [
path('api/v1/', include(router.urls)),
# more paths for auth, admin, etc..
]
Thoughts?
I know you said you wanted to avoid adding an extra action but in my opinion it's the simplest way to update your existing views for bulk create/update/delete.
You can create a mixin that you add to your views that will handle everything, you'd just be changing one line in your existing views and serializers.
Assuming your ListSerializer look similar to the DRF documentation the mixins would be as follows.
core/serializers.py
class BulkUpdateSerializerMixin:
"""
Mixin to be used with BulkUpdateListSerializer & BulkUpdateRouteMixin
that adds the ID back to the internal value from the raw input data so
that it's included in the validated data.
"""
def passes_test(self):
# Must be an update method for the ID to be added to validated data
test = self.context['request'].method in ('PUT', 'PATCH')
test &= self.context.get('bulk_update', False)
return test
def to_internal_value(self, data):
ret = super().to_internal_value(data)
if self.passes_test():
ret['id'] = self.fields['id'].get_value(data)
return ret
core/views.py
class BulkUpdateRouteMixin:
"""
Mixin that adds a `bulk_update` API route to a view set. To be used
with BulkUpdateSerializerMixin & BulkUpdateListSerializer.
"""
def get_object(self):
# Override to return None if the lookup_url_kwargs is not present.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
if lookup_url_kwarg in self.kwargs:
return super().get_object()
return
def get_serializer(self, *args, **kwargs):
# Initialize serializer with `many=True` if the data passed
# to the serializer is a list.
if self.request.method in ('PUT', 'PATCH'):
data = kwargs.get('data', None)
kwargs['many'] = isinstance(data, list)
return super().get_serializer(*args, **kwargs)
def get_serializer_context(self):
# Add `bulk_update` flag to the serializer context so that
# the id field can be added back to the validated data through
# `to_internal_value()`
context = super().get_serializer_context()
if self.action == 'bulk_update':
context['bulk_update'] = True
return context
#action(detail=False, methods=['put'], url_name='bulk_update')
def bulk_update(self, request, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(
queryset,
data=request.data,
many=True,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data, status=status.HTTP_200_OK)
Then you would just inherit from the mixins
class MyModelSerializer(BulkUpdateSerializerMixin
serializers.ModelSerializer):
class Meta:
model = MyModel
list_serializer_class = BulkUpdateListSerializer
class MyModelViewSet(BulkUpdateRouteMixin,
viewsets.ModelViewSet):
...
And your PUT request would just have to point to '/api/v1/my-model/bulk_update'
Updated mixins that don't require extra viewset action:
For bulk operations submit a POST request to the list view with data as a list.
class BulkUpdateSerializerMixin:
def passes_test(self):
test = self.context['request'].method in ('POST',)
test &= self.context.get('bulk', False)
return test
def to_internal_value(self, data):
ret = super().to_internal_value(data)
if self.passes_test():
ret['id'] = self.fields['id'].get_value(data)
return ret
In get_serializer() there's a check to ensure that only POST requests can be accepted for bulk operations. If it's a POST and the request data is a list then add a flag so the ID field can be added back to the validated data and your ListSerializer can handle the bulk operations.
class BulkUpdateViewSetMixin:
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
if self.request.method in ('POST',):
data = kwargs.get('data', None)
is_bulk = isinstance(data, list)
kwargs['many'] = is_bulk
kwargs['context']['bulk'] = is_bulk
return serializer_class(*args, **kwargs)
def create(self, request, *args, **kwargs):
if isinstance(request.data, list):
return self.bulk_update(request)
return super().create(request, *args, **kwargs)
def bulk_update(self, request):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(
queryset,
data=request.data,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data, status=status.HTTP_200_OK)
I've tested that this works but I have no idea how it will affect API schema documentation.
When I attempt to test my Create/POST route for my Django Rest Framework API I receive a response status code of 401 with the error detail telling me ErrorDetail(string=u'Authentication credentials were not provided.', code=u'not_authenticated'). The weird thing is I Django tells me I'm authenticated when I check is is_authenticated.
Does anyone have an idea what might be causing this? All relevant code provided below.
# test_api.py
def authorise_user_and_test_is_authenticated(self, user_id):
"""
Log in user and test this is successful
"""
user = User.objects.get(pk=user_id)
self.client.login(username=user.username, password=user.password)
authorised_user = auth.get_user(self.client)
return self.assertTrue(user.is_authenticated())
def test_create_project(self):
'''
When given valid parameters a project is created.
'''
user = User.objects.get(username="user_001")
self.authorise_user_and_test_is_authenticated(user.id) # pass of authenication and auth testing to method, when tested with is_authenicated() it returns true.
response = self.client.post('/api/user/{0}/project/create/'.format(user.id),
json.dumps({"model_name": "POSTed Project",
"description": "Project tested by posting",
"shared_users[]": [2]
}),
content_type='application/json')
self.assertEqual(response.status_code, 201)
# views.py
class MyCreateView(generics.GenericAPIView):
pass
serializer_class = FerronPageCreateAndUpdateSerializer
def get_queryset(self):
return User.objects.filter(pk=self.kwargs.get('user'))
def post(self, request, format=None, **kwargs):
# This dictionary is used to ensure that the last_modified_by field is always updated on post to be the current user
print request.data
request_data = {
'user': request.user.id,
'model_name': request.data['model_name'],
'description': request.data['description'],
'last_modified_by': request.user.id,
'shared_users': request.data.getlist('shared_users[]', [])
}
serializer = FerronPageCreateAndUpdateSerializer(data=request_data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# settings.py
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication'
),
}
# url matcher
url(r'^user/(?P<user>\d+)/project/create/$', MyCreateView.as_view(), name='create-project')
class FerronPageCreateAndUpdateSerializer(serializers.ModelSerializer):
shared_users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), read_only=False)
description = serializers.CharField(max_length=300, trim_whitespace=True, required=False, allow_blank=True)
class Meta:
model = Project
fields = [
'pk',
'user',
'data',
'model_name',
'description',
'created_at',
'date_modified',
'shared_users',
'last_modified_by'
]
Turn's out the issue was here self.client.login(username=user.username, password=user.password) inside the authorise_user_and_test_is_authenticated(self, user_id) method.
The problem was that I was using the password an instance of a user I had already created. This meant when I gave the argument password=user.password, I was trying to log in using a password that had already been hashed. What I needed to do instead was log in with the original unhashed version of the password e.g. password='openseasame'.
Im trying to perform partial update in DRF using angular $http.
In my DRF model viewset i override the partial_update function (server side).
class AnimalTreatmentViewSet(viewsets.ModelViewSet):
queryset = MyObject.objects.all()
serializer_class = MyObjectSerializer
def create(self, request):
# It works with $http.post()
pass
def update(self, request, pk=None):
# It works with $http.put()
pass
def list(self, request):
# It works with $http.get()
pass
def partial_update(self, request, pk=None):
# This one wont work with $http.patch()
instance = self.get_object()
serializer = self.serializer_class(instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
In the client side angular handle the user request.
$http.patch('/api/my_viewset_url/1', data);
But i got this response Method Not Allowed (PATCH): /api/my_viewset_url/1
When using $http.get() request with DRF model viewset list(self, request) it works well for getting a list same goes for $http.post() with def create(self, request) for creating object and $http.put() with def update(self, request) for updating object.
What's wrong? or what is the correct http verb for partial_update in DRF model viewset
It seems like the URL is missing a trailing slash.
Related documentation:
By default the URLs created by SimpleRouter/DefaultRouter are appended with a
trailing slash. This behavior can be modified by setting the trailing_slash argument to False when instantiating the router. For example:
router = routers.DefaultRouter(trailing_slash=False)
I have developed a simple webservice, but failed to use post with Django Rest Framework as it complains about CSRF:
"detail": "CSRF Failed: CSRF cookie not set."
Removing the api_view decorator does stop the message from appearing but then I won't be able to access the request.data. I think that the api_view does check CSRF although I added the csrf_exempt decorator.
This is my view:
#permission_classes((IsAuthenticated, ))
#csrf_exempt
#api_view(['POST'])
def get_stats(request):
"""
Returns the stats available.
"""
user = request.user
if request.method == 'POST':
serializer = StatsRequestSerializer(data=request.data)
stats_request = serializer.data
return JSONResponse(stats_request)
#serializer = QuizSerializer(user.quizes.all(), many=True)
#return JSONResponse(serializer.data)
response = ActionResponse(status='error', error='Invalid request')
serializer = ActionResponseSerializer(response)
return JSONResponse(serializer.data, status=400)
This is my model:
class StatsRequest(models.Model):
"""
A model which describes a request for some stats for specific users.
"""
start_date = models.DateField()
end_date = models.DateField()
and this is my request POST:
{"start_date" : "1992-01-15", "end_date" : "1992-01-15" }
Any ideas?
More info:
AUTHENTICATION_BACKENDS = (
'social.backends.facebook.FacebookOAuth2',
'social.backends.google.GoogleOAuth2',
'django.contrib.auth.backends.ModelBackend'
)
So, after trying to figure this out for a couple of hours I finally did it.
Tracing the source code of DRF and Django lead me to believe that I need to find a workaround for this as the CSRF verification is made explicitly even if turned off, probably the CSRF check is being made at the api_view decorator. So I simply created my own decorator:
from functools import wraps
from django.utils.decorators import available_attrs, decorator_from_middleware
def csrf_clear(view_func):
"""
Skips the CSRF checks by setting the 'csrf_processing_done' to true.
"""
def wrapped_view(*args, **kwargs):
request = args[0]
request.csrf_processing_done = True
return view_func(*args, **kwargs)
return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)
and my view with the new decorator:
#csrf_clear
#api_view(['POST'])
#permission_classes((IsAuthenticated, ))
def get_stats(request):
"""
Returns the stats available.
"""
user = request.user
if request.method == 'POST':
serializer = StatsRequestSerializer(data=request.data)
if serializer.is_valid():
stats_request = serializer.data
return JSONResponse(stats_request)
#serializer = QuizSerializer(user.quizes.all(), many=True)
#return JSONResponse(serializer.data)
response = ActionResponse(status='error', error='Invalid request')
serializer = ActionResponseSerializer(response)
return JSONResponse(serializer.data, status=400)
urls.py
from django.views.decorators.csrf import csrf_exempt
urlpatterns = [
url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^snippets/(?P<pk>[0-9]+)/$', csrf_exempt(views.SnippetDetail.as_view())),
]
views.py
from django.views.decorators.csrf import csrf_exempt
from rest_framework.views import APIView
class SnippetList(APIView):
#csrf_exempt
#need_post_parameters([PARAM_MESSAGE_OBJ])
def post(self, request, *args, **kwargs):
data = request.POST.get(PARAM_MESSAGE_OBJ)
try:
message_obj = json.loads(data)
except Exception as e:
return HttpResponseBadRequest(error_json("Could not parse JSON"))
http://www.chenxm.cc/post/509.html