I am creating a REST-API with django and the django rest framework, therefore I am using ReadOnlyModelViewSet. In my unit test for testing create / POST method, I am expecting HTTP_405_METHOD_NOT_ALLOWED but actually it is HTTP_403_FORBIDDEN. For the update / PUT it is HTTP_405 as expected. Is this behavior the default behavior or do I have a dumb bug?
And yes the user is authenticated.
The Viewset:
class StudentSolutionReadOnlyModelViewSet(viewsets.ReadOnlyModelViewSet):
queryset = StudentSolution.objects.all()
serializer_class = StudentSolutionSerializer
def get_permissions(self):
if self.request.method == 'POST':
return (permissions.IsAuthenticated(), )
return (permissions.IsAuthenticated(), IsStudentSolutionOwnerOrAdmin(),)
def get_queryset(self):
if self.request.user.is_staff:
return StudentSolution.objects.all(
course_assignment=self.kwargs['course_assignment_pk']
).order_by('student__last_name', 'course_assignment__due_date')
return StudentSolution.objects.filter(
student=self.request.user,
course_assignment=self.kwargs['course_assignment_pk']
).order_by('course_assignment__due_date')
Edit 1:
class StudentSolutionReadOnlyModelViewSetTests(TestCase):
def setup(self):
#[..]
def test_create_user_not_allowed(self):
data = {
'course_assignment': self.course_assignment.id,
'student': self.user.id
}
url = self.generate_url(self.course_assignment.id)
self.csrf_client.credentials(HTTP_AUTHORIZATION='JWT ' + self.user_token)
resp = self.csrf_client.post(
self.url,
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(resp.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
I am using the same authentication and csrf stuff for all methods, as well as generate_url method.
Related
I am working on a Quiz Application using DjangoRestFramework and ReactJS. My App is comprised of two apps, api and frontend. In my api app, I have multiple API views for many different things. One of my API Views is called JoinQuiz. When I call it on my frontend, I get this error in the console:
Forbidden: /api/join-quiz
[16/Dec/2020] "POST /api/join-quiz HTTP/1.1" 403 58
I don't think my problem is due to a CSRF error because my other API views are working perfectly fine. I may be wrong on this point.
[16/Dec/2020] "GET /api/get-question?id=3 HTTP/1.1" 200 10009
[17/Dec/2020 01:15:47] "POST /api/create-quiz HTTP/1.1" 201 11959
I suspect that my request.session may be the issue because when I go directly to /api/join-quiz and make a POST request with my code, nothing goes wrong and I have a successful post.
Files
Views.py
class QuizView(generics.ListAPIView):
queryset = Quiz.objects.all()
serializer_class = QuizSerializer
class GetQuiz(APIView):
""" Searches for a quiz given its code and returns the Quiz with is_host info"""
serializer_class = QuizSerializer
lookup_url_kwarg = 'code'
def get(self, request, format=None): # This is an HTTP GET request
code = request.GET.get(self.lookup_url_kwarg)
if code != None: # Check if code is not equal to None
quiz = Quiz.objects.filter(code=code)
if len(quiz) > 0: # If there is a quiz...
data = QuizSerializer(quiz[0]).data
data['is_host'] = self.request.session.session_key == quiz[0].host
return Response(data, status=status.HTTP_200_OK)
return Response({'Quiz not found': 'Invalid Code'}, status=status.HTTP_404_NOT_FOUND)
return Response({'Bad Request': 'Code Parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
class CreateQuizView(APIView):
"""Creates A new Quiz given nested question and answer data"""
serializer_class = QuizSerializer
def post(self, request, format=None):
""" Create the User's Account first"""
if not self.request.session.exists(self.request.session.session_key):
self.request.session.create()
data = request.data
data['host'] = self.request.session.session_key
serializer = self.serializer_class(data=data)
if serializer.is_valid():
quiz = serializer.create(validated_data=data)
self.request.session['quiz_code'] = quiz.code
return Response(
self.serializer_class(quiz).data,
status=status.HTTP_201_CREATED
)
return Response({'Bad Request': 'Invalid Data'}, status=status.HTTP_400_BAD_REQUEST)
class JoinQuiz(APIView):
"""Join a quiz based on the quiz code"""
lookup_url_kwarg = 'code'
def post(self, request, format=None):
if not self.request.session.exists(self.request.session.session_key):
self.request.session.create()
print(self.request.session.session_key)
code = request.data.get(self.lookup_url_kwarg)
if code != None:
quiz_result = Quiz.objects.filter(code=code)
if len(quiz_result) > 0:
self.request.session['quiz_code'] = code
return Response({'message': 'Quiz Joined!'}, status=status.HTTP_200_OK)
return Response({'Quiz Not Found': 'Invalid Quiz Code'}, status=status.HTTP_404_NOT_FOUND)
return Response({'Bad Request': 'Invalid Post Data'}, status=status.HTTP_400_BAD_REQUEST)
serializers.py
class QuizSerializer(serializers.ModelSerializer):
questions = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Quiz
fields = ['id', 'code', 'questions', 'name']
def create(self, validated_data):
questions_data = validated_data.pop('questions')
quiz = Quiz.objects.create(**validated_data)
for question_data in questions_data:
answers_data = question_data.pop('answers')
question = Question.objects.create(quiz=quiz, **question_data)
for answer_data in answers_data:
Answer.objects.create(question=question, **answer_data)
return quiz
Frontend Request
const quizButtonPressed = () => {
const requestOptions = {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
code: quizCode
})
};
fetch('/api/join-quiz', requestOptions)
.then((response) => {
if (response.ok) {
props.history.push(`/play/${quizCode}`);
} else {
setError("Quiz not found")
}
})
.catch((error) => console.log(error));
}
EDIT
I followed the solution from #Blusky and it worked!
Following the Django docs link from #Blusky solved the problem: https://docs.djangoproject.com/en/3.1/ref/csrf/#ajax
Even though you checked it, it might be a CSRF problem. You should check the body of the 403 Error, it might contains further information.
Only when authenticated, POST request on Django requires a CSRF token (it might be why your first POST is working)
If it's the case, you can check this snippet: https://docs.djangoproject.com/en/3.1/ref/csrf/#ajax
Docs says only mapping of GET
user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})
tests.py:
def test_admin_can_create_role(userprofiles, aoo_admin, bug_manager, note_admin):
aoo = User.objects.get(username='aoo')
factory = APIRequestFactory()
view = RoleViewSet.as_view()
url = reverse('api:role-list')
data = {
'name': 'FirstAdmin',
'type': Role.RoleType.admin,
'company': 1,
}
request = factory.post(url, data=data, format='json')
force_authenticate(request, user=aoo)
response = view(request)
assert 201 == response.data
viewsets.py
class RoleViewSetPermission(permissions.BasePermission):
message = 'Only Manager or Admin are allowed'
def has_permission(self, request, view):
user = request.user
return user.has_perm('roles.add_role') \
and user.has_perm('roles.change_role') \
and user.has_perm('roles.delete_role')
class RoleViewSet(viewsets.ModelViewSet):
permission_classes = (RoleViewSetPermission,)
queryset = Role.objects.all()
serializer_class = RoleSerializer
filter_backends = (filters.DjangoFilterBackend, SearchFilter)
filter_class = RoleFilter
search_fields = ('name', 'description', 'user__username', 'company__name', 'company__name_th')
def filter_queryset(self, queryset):
try:
company = self.request.user.user_profile.companyappid.company
except AttributeError:
logger.error(f'{self.request.user} has AttributeError')
return Role.objects.none()
else:
logger.info(f'{self.request.user} is {company} member')
return queryset.filter(company=company)
Trackback:
cls = <class 'poinkbackend.apps.roles.api.viewsets.RoleViewSet'>, actions = None, initkwargs = {}
#classonlymethod
def as_view(cls, actions=None, **initkwargs):
"""
Because of the way class based views create a closure around the
instantiated view, we need to totally reimplement `.as_view`,
and slightly modify the view function that is created and returned.
"""
# The suffix initkwarg is reserved for identifying the viewset type
# eg. 'List' or 'Instance'.
cls.suffix = None
# actions must not be empty
if not actions:
> raise TypeError("The `actions` argument must be provided when "
"calling `.as_view()` on a ViewSet. For example "
"`.as_view({'get': 'list'})`")
E TypeError: The `actions` argument must be provided when calling `.as_view()` on a ViewSet. For example `.as_view({'get': 'list'})`
../../.pyenv/versions/3.6.3/envs/poink/lib/python3.6/site-packages/rest_framework/viewsets.py:55: TypeError
Question:
How to do force_authenticate and request.post to the viewsets?
I have no problem with get. It has an answer already in the SO
References:
http://www.django-rest-framework.org/api-guide/viewsets/
I have to use APIClient not APIRequestFactory.
I though it has only one way to do testing.
Here is my example.
def test_admin_can_create_role(userprofiles, aoo_admin, bug_manager, note_admin):
aoo = User.objects.get(username='aoo')
client = APIClient()
client.force_authenticate(user=aoo)
url = reverse('api:role-list')
singh = Company.objects.get(name='Singh')
data = {
'name': 'HairCut',
'type': Role.RoleType.admin,
'company': singh.id, # Must be his companyid. Reason is in the RoleSerializer docstring
}
response = client.post(url, data, format='json')
assert 201 == response.status_code
TL;DR
I am looking for a way to clear the cache after a request, or completely disable it when running tests. Django REST Framework seems to cache the results and I need a way around this.
Long version and code
Well this turned out to behave very weird as I kept testing it. In the end, I got it to work, but I really don't like my workaround and in the name of knowledge, I have to find out why this happens and how to solve this problem properly.
So, I have an APITestCase class declared like this:
class UserTests(APITestCase):
Inside this class, I have a test function for my user-list view, as I have a custom queryset depending on the permissions. To clear things up:
a superuser can get the whole users list (4 instances returned),
staff members cannot see superusers (3 instances returned),
normal users can only get 1 result, their own user (1 instance returned)
The test function version that works:
def test_user_querysets(self):
url = reverse('user-list')
# Creating a user
user = User(username='user', password=self.password)
user.set_password(self.password)
user.save()
# Creating a second user
user2 = User(username='user2', password=self.password)
user2.set_password(self.password)
user2.save()
# Creating a staff user
staff_user = User(username='staff_user', password=self.password, is_staff=True)
staff_user.set_password(self.password)
staff_user.save()
# Creating a superuser
superuser = User(username='superuser', password=self.password, is_staff=True, is_superuser=True)
superuser.set_password(self.password)
superuser.save()
# SUPERUSER
self.client.logout()
self.client.login(username=superuser.username, password=self.password)
response = self.client.get(url)
# HTTP_200_OK
self.assertEqual(response.status_code, status.HTTP_200_OK)
# All users contained in list
self.assertEqual(response.data['extras']['total_results'], 4)
# STAFF USER
self.client.logout()
self.client.login(username=staff_user.username, password=self.password)
response = self.client.get(url)
# HTTP_200_OK
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Superuser cannot be contained in list
self.assertEqual(response.data['extras']['total_results'], 3)
# REGULAR USER
self.client.logout()
self.client.login(username=user2.username, password=self.password)
response = self.client.get(url)
# HTTP_200_OK
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Only 1 user can be returned
self.assertEqual(response.data['extras']['total_results'], 1)
# User returned is current user
self.assertEqual(response.data['users'][0]['username'], user2.username)
As you see, I am testing user permissions in this order: superuser, staff, normal user. And this works, so...
Funny thing:
If I change the order of the tests, and start with normal user, staff, superuser, the tests fail. The response from the first request gets cached, and then I get the same response when I log in as staff user, so the number of results is again 1.
The version that doesn't work:
it's exactly the same as before, only the tests are made in reverse order
def test_user_querysets(self):
url = reverse('user-list')
# Creating a user
user = User(username='user', password=self.password)
user.set_password(self.password)
user.save()
# Creating a second user
user2 = User(username='user2', password=self.password)
user2.set_password(self.password)
user2.save()
# Creating a staff user
staff_user = User(username='staff_user', password=self.password, is_staff=True)
staff_user.set_password(self.password)
staff_user.save()
# Creating a superuser
superuser = User(username='superuser', password=self.password, is_staff=True, is_superuser=True)
superuser.set_password(self.password)
superuser.save()
# REGULAR USER
self.client.logout()
self.client.login(username=user2.username, password=self.password)
response = self.client.get(url)
# HTTP_200_OK
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Only 1 user can be returned
self.assertEqual(response.data['extras']['total_results'], 1)
# User returned is current user
self.assertEqual(response.data['users'][0]['username'], user2.username)
# STAFF USER
self.client.logout()
self.client.login(username=staff_user.username, password=self.password)
response = self.client.get(url)
# HTTP_200_OK
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Superuser cannot be contained in list
self.assertEqual(response.data['extras']['total_results'], 3)
# SUPERUSER
self.client.logout()
self.client.login(username=superuser.username, password=self.password)
response = self.client.get(url)
# HTTP_200_OK
self.assertEqual(response.status_code, status.HTTP_200_OK)
# All users contained in list
self.assertEqual(response.data['extras']['total_results'], 4)
I am working in python 2.7 with the following package versions:
Django==1.8.6
djangorestframework==3.3.1
Markdown==2.6.4
MySQL-python==1.2.5
wheel==0.24.0
UPDATE
I am using the default django cache, meaning I haven't put anything about cache in the django settings.
As suggested, I tried disabling the default Django cache:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
)
}
The problem stands on.
Even though I don't think the problem is located here, this is my UserViewSet:
api.py (the important part)
class UserViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet
):
queryset = User.objects.all()
serializer_class = UserExpenseSerializer
permission_classes = (IsAuthenticated, )
allowed_methods = ('GET', 'PATCH', 'OPTIONS', 'HEAD')
def get_serializer_class(self):
if self.action == 'retrieve':
return UserExpenseSerializer
return UserSerializer
def get_queryset(self):
if(self.action == 'list'):
return User.objects.all()
if self.request.user.is_superuser:
return User.objects.all()
if self.request.user.is_staff:
return User.objects.exclude(is_superuser=True)
return User.objects.filter(pk = self.request.user.id)
def list(self, request):
filter_obj = UsersFilter(self.request)
users = filter_obj.do_query()
extras = filter_obj.get_extras()
serializer = UserSerializer(users, context={'request' : request}, many=True)
return Response({'users' : serializer.data, 'extras' : extras}, views.status.HTTP_200_OK)
filters.py
class UsersFilter:
offset = 0
limit = 50
count = 0
total_pages = 0
filter_params = {}
def __init__(self, request):
if not request.user.is_superuser:
self.filter_params['is_superuser'] = False
if (not request.user.is_superuser and not request.user.is_staff):
self.filter_params['pk'] = request.user.id
# Read query params
rpp = request.query_params.get('rpp') or 50
page = request.query_params.get('page') or 1
search_string = request.query_params.get('search')
# Validate
self.rpp = int(rpp) or 50
self.page = int(page) or 1
# Set filter
set_if_not_none(self.filter_params, 'username__contains', search_string)
# Count total results
self.count = User.objects.filter(**self.filter_params).count()
self.total_pages = int(self.count / self.rpp) + 1
# Set limits
self.offset = (self.page - 1) * self.rpp
self.limit = self.page * self.rpp
def get_filter_params(self):
return self.filter_params
def get_offset(self):
return self.offset
def get_limit(self):
return self.limit
def do_query(self):
users = User.objects.filter(**self.filter_params)[self.offset:self.limit]
return users
def get_query_info(self):
query_info = {
'total_results' : self.count,
'results_per_page' : self.rpp,
'current_page' : self.page,
'total_pages' : self.total_pages
}
return query_info
UPDATE 2
As Linovia pointed out, the problem was not cache or any other DRF problem, but the filter. Here's the fixed filter class:
class UsersFilter:
def __init__(self, request):
self.filter_params = {}
self.offset = 0
self.limit = 50
self.count = 0
self.total_pages = 0
self.extras = {}
if not request.user.is_superuser:
# and so long...
Actually you create a new user which should make 2 users and you assert the length against 3. Not going to work even without caching.
Edit:
So you actually have you issue because the use of mutables objects at the class level.
Here's the evil code:
class UsersFilter:
filter_params = {}
def __init__(self, request):
if not request.user.is_superuser:
self.filter_params['is_superuser'] = False
Which should actually be:
class UsersFilter:
def __init__(self, request):
filter_params = {}
if not request.user.is_superuser:
self.filter_params['is_superuser'] = False
Otherwise UsersFilter.filter_params will be kept from one request to another and never resets. See http://www.toptal.com/python/python-class-attributes-an-overly-thorough-guide for more details about this.
You can disable caching in debug mode by adding this to your settings.py
if DEBUG:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
https://docs.djangoproject.com/en/dev/topics/cache/?from=olddocs/#dummy-caching-for-development
You can then disable caching by toggling DEBUG in settings.py or by having separate develop/test and /deploy settings.py files.
If you want to avoid separate files or toggling, then you can set DEBUG to true for certain tests with the override_settings decorator:
from django.test.utils import override_settings
from django.test import TestCase
from django.conf import settings
class MyTest(TestCase):
#override_settings(DEBUG=True)
def test_debug(self):
self.assertTrue(settings.DEBUG)
I have code that read like this to check if POST parameters are included on the request:
def login(request):
required_params = frozenset(('email', 'password'))
if required_params <= frozenset(request.POST):
# 'email' and 'password' are included in the POST request
# continue as normal
pass
else:
return HttpResponseBadRequest()
When the list of required POST parameters is big, this code gets messy. What I would like to do is something like:
#required_POST_params('email', 'password')
def login(request):
# 'email' and 'password' are here always!
pass
Then I'm confident that both 'email' and 'password' POST parameters are included in the request, because if not, the request would automatically return HttpResponseBadRequest().
Is there a way that Django allows me to do this, and if it doesn't, how can I do it by myself with a decorator?
You would need a custom decorator, but you can take require_http_methods as a base example:
def require_post_params(params):
def decorator(func):
#wraps(func, assigned=available_attrs(func))
def inner(request, *args, **kwargs):
if not all(param in request.POST for param in params):
return HttpResponseBadRequest()
return func(request, *args, **kwargs)
return inner
return decorator
Example usage:
#require_post_params(params=['email', 'password'])
def login(request):
# 'email' and 'password' are here always!
pass
FYI, require_http_methods source code.
i'm sharing my solution;
__author__ = 'yagmurs'
from copy import deepcopy
from rest_framework import status
from rest_framework.response import Response
def require_params(*params):
def decorator(fn):
def wrapped_function(request, *args, **kwargs):
"""
Decorator for django rest service to meet both GET and POST request
"""
error = deepcopy(REQUEST_INVALID_400)
is_param_missing = False
for param in params:
if not get_param_from_request(param, request):
error['result_message'] += param + ", "
is_param_missing = True
if is_param_missing:
error['result_message'] = error['result_message'][:-2]
return Response(error, status=status.HTTP_400_BAD_REQUEST)
else:
return fn(request, *args, **kwargs)
return wrapped_function
return decorator
def get_param_from_request(param, request):
if request.method == 'POST':
return request.data.get(param)
else:
return request.query_params.get(param)
Try with this.
Instead of require.POST(), try with require.POST('email', 'password').
I'm trying to implement a simple Django service with a RESTful API using tastypie. My problem is that when I try to create a WineResource with PUT, it works fine, but when I use POST, it returns a HTTP 501 error. Reading the tastypie documentation, it seems like it should just work, but it's not.
Here's my api.py code:
class CustomResource(ModelResource):
"""Provides customizations of ModelResource"""
def determine_format(self, request):
"""Provide logic to provide JSON responses as default"""
if 'format' in request.GET:
if request.GET['format'] in FORMATS:
return FORMATS[request.GET['format']]
else:
return 'text/html' #Hacky way to prevent incorrect formats
else:
return 'application/json'
class WineValidation(Validation):
def is_valid(self, bundle, request=None):
if not bundle.data:
return {'__all__': 'No data was detected'}
missing_fields = []
invalid_fields = []
for field in REQUIRED_WINE_FIELDS:
if not field in bundle.data.keys():
missing_fields.append(field)
for key in bundle.data.keys():
if not key in ALLOWABLE_WINE_FIELDS:
invalid_fields.append(key)
errors = missing_fields + invalid_fields if request.method != 'PATCH' \
else invalid_fields
if errors:
return 'Missing fields: %s; Invalid fields: %s' % \
(', '.join(missing_fields), ', '.join(invalid_fields))
else:
return errors
class WineProducerResource(CustomResource):
wine = fields.ToManyField('wines.api.WineResource', 'wine_set',
related_name='wine_producer')
class Meta:
queryset = WineProducer.objects.all()
resource_name = 'wine_producer'
authentication = Authentication() #allows all access
authorization = Authorization() #allows all access
class WineResource(CustomResource):
wine_producer = fields.ForeignKey(WineProducerResource, 'wine_producer')
class Meta:
queryset = Wine.objects.all()
resource_name = 'wine'
authentication = Authentication() #allows all access
authorization = Authorization() #allows all access
validation = WineValidation()
filtering = {
'percent_new_oak': ('exact', 'lt', 'gt', 'lte', 'gte'),
'percentage_alcohol': ('exact', 'lt', 'gt', 'lte', 'gte'),
'color': ('exact', 'startswith'),
'style': ('exact', 'startswith')
}
def hydrate_wine_producer(self, bundle):
"""Use the provided WineProducer ID to properly link a PUT, POST,
or PATCH to the correct WineProducer instance in the db"""
#Workaround since tastypie has bug and calls hydrate more than once
try:
int(bundle.data['wine_producer'])
except ValueError:
return bundle
bundle.data['wine_producer'] = '/api/v1/wine_producer/%s/' % \
bundle.data['wine_producer']
return bundle
Any help is greatly appreciated! :-)
This usually means that you are sending the POST to a detail uri, e.g. /api/v1/wine/1/. Since POST means treat the enclosed entity as a subordinate, sending the POST to the list uri, e.g. /api/v1/wine/, is probably what you want.