So I found this question:
Django Rest Framework testing save POST request data
And I understood that the data created with POST request should be accessible as long as the function is running. So I made the whole test in one function:
class PostMovieAPITest(APITestCase):
def test_correct_request(self):
title = 'Snatch'
response = self.client.post('/movies/', data={'title': title}, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
movies = Movie.objects.all()
self.assertTrue(Movie.objects.get(title=title))
The problem is, Movie.objects.all() is empty, even though I send a CREATE/POST request in the same function. The API works fine when I do `manage.py runserver' and test it in browser. But how can write a proper test to check if data is actually saved in database?
urls.py:
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from api import views
router = routers.DefaultRouter()
router.register(r'movies', views.MovieViewSet)
urlpatterns = [
path('', include(router.urls)),
path('admin/', admin.site.urls),
]
views.py:
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def create(self, request, *args, **kwargs):
title = request.data.get('title')
if not title:
return Response({'Error': "Body should be {'title':'The title of the Movie'}"}, status=status.HTTP_400_BAD_REQUEST)
data = get_data_from_omdb(title)
if len(data) == 0:
return Response({"Error": "Title does not exist in OMDB database"}, status=status.HTTP_400_BAD_REQUEST)
serializer = MovieSerializer(data=data, context={'request': request})
if serializer.is_valid(raise_exception=False):
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Found the problem thanks to Ozgur Akcali's comment. I couldn't get the movie because Movie object was created based on data from external API, including the title.
"Hire me!" -> "Hire Me!"
Sometimes scripting makes me want to kill. Thanks a lot and sorry for wasting your time.
def test_correct_request(self):
title = 'Hire Me!' # was: 'Hire me!'
response = self.client.post('/movies/', data={'title': title}, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(Movie.objects.get(title=title))
Related
I'm trying to create a test for creating a new post, but I'm getting Error 403. I've read the questions on this topic, but either they were not about testing or the solution they provided did not work (I'll provide info about what I've tried so far).
Here is my code:
urls.py:
app_name = "blog_api"
urlpatterns = [
path("<str:pk>/", PostDetail.as_view(), name="detail_create"),
path("", PostList.as_view(), name="list_create"),
]
and my permissions.py:
class PostUserWritePermission(permissions.BasePermission):
message = "Editing posts is restricted to the admin and author of the post only."
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_superuser or request.user == obj.author
And here is my views.py:
class PostList(generics.ListCreateAPIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Post.post_objects.all()
serializer_class = PostSerializer
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [PostUserWritePermission]
queryset = Post.post_objects.all()
serializer_class = PostSerializer
And finally, my tests.py:
class PostTests(APITestCase):
def test_view_posts(self):
url = reverse("blog_api:list_create")
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_post(self):
self.test_category = Category.objects.create(name="test category")
self.test_user = User.objects.create_user(
username="test_user",
password="test_password",
)
data = {
"title": "new",
"author": 1,
"excerpt": "new",
"content": "new",
"slug": "new",
}
url = reverse("blog_api:list_create")
response = self.client.post(url, data, format="json")
print(response.data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
So, when I test using py manage.py test command, the first test is ok, but the second one returns error 403, and when I tried to see the response message using print(response.data) this is what's printed:
{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}
I should also mention that I have this setting in my settings.py file:
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
}
I did some searching about this, this one question was a bit similar to my problem, but I create my user using create_user method, thus his solution is irrelevant to my question.
What am I missing here?
WHAT I"VE TRIED:
I tried to comment out the permision class PostUserWritePermission in the PostDetail view, but got the same error nonetheless.
Also I tried to solve this by adding TokenAuthentication to my settings, but this didn't work neither.
I Created a view for article model and I have the data in my database but when I go on url then it's showing empty array
my below code
view.py
class ArticleView(RetrieveAPIView, RetrieveUpdateAPIView, RetrieveDestroyAPIView):
serializer_class = ArticleSerializer
permission_classes = [AllowAny]
pagination_class = ArticleSizeLimitPagination
def get_queryset(self):
return Article.objects.filter().order_by('-updated_on')
def get_object(self):
try:
return Article.objects.get(id=self.request.data['id'], is_deleted=False)
except Exception:
return None
def perform_create(self, serializer):
return serializer.save(user=self.request.user)
def destroy(self, request, *args, **kwargs):
try:
instance = self.get_object()
instance.is_deleted = True
instance.save()
return Response(data='delete success')
except Http404:
return Response(status=status.HTTP_204_NO_CONTENT)
urls.py
urlpatterns = [
path('', ArticleView.as_view()),
path('<uuid:pk>/', ArticleView.as_view()),
]
When I run the url then
HTTP 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"sub_title": "",
"upvote": null,
"title": ""
}
Where did I do wrong? Is there issue in def function for RetrieveAPIView?
You created a view with selected mixins. You will be able to get one instance, update or delete. Is this what you want? Here you have the list of available mixins: DRF docs.
When you open the URL in the browser you probably get the DRF browsable API. It will show you nice forms to interact with your endpoints. If you want to get the article from the endpoint, you should provide URL with id (pk (where pk is primary key)).
Your URLs looks wrong. Please try:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(
r"articles",
ArticleView
)
urlpatterns = [
url(r"/", include(router.urls)),
]
I would recommend you to use ModelViewSet instead of building view from mixins. It should be easier.
class ArticleView(ModelViewSet):
Please make it works with ModelViewSet (easier solution) and then try to narrow down with a harder solution, by defining mixins.
I am trying to build a simple REST API.
I tried adding a viewset, somehow I get an error that there is no such attribute.
If I remove the viewset and just run using the APIView, it loads just fine. I am stuck. What could be the problem? What should I do to make it work?
Here's the rest_profiles.views.py FILE:
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import HelloSerializer
# Create your views here.
class HelloApiView(APIView):
'''Test API View'''
serializer_class = HelloSerializer
def get(self, request, format=None):
'''Returns a list of API features'''
an_apiview = [
'Uses HTTP methods as functions (get, psot, put, patch, delete)',
'Similar to Django View',
'Mapped manually to URLs'
]
return Response({'message': 'Hello from HelloAPIVIew', 'an_apiview': an_apiview})
def post(self, request):
'''Create Hello Message'''
serializer = HelloSerializer(data=request.data)
if serializer.is_valid():
name = serializer.data.get('name')
message = 'Hello {0}'.format(name)
return Response({'message': message})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def put(self, request):
'''Handles Updates'''
serializer = HelloSerializer(data=request.data)
return Response({'message': 'put'})
def patch(self, request, pk=None):
'''Handles partial Updates'''
serializer = HelloSerializer(data=request.data)
return Response({'message': 'patch'})
def delete(self, request, pk=None):
'''Handles deleting items'''
serializer = HelloSerializer(data=request.data)
return Response({'message': 'delete'})
class HelloViewSet(viewsets.ViewSet):
'''Test API Viewset'''
def list(self, request):
'''Return Hello Message'''
a_viewset = [
'Uses Actions (list, create, retrieve, update, partial_update)',
'automatically maps to URLs using Router',
'More functionality with less code'
]
return Response({'message': 'Hello', 'a_viewset': a_viewset})
and Here is the urls.py file:
from django.conf.urls import url
from django.conf.urls import include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register('hello-viewset', views.HelloViewSet,
base_name='hello-viewset')
urlpatterns = [
url('hello-view/', views.HelloApiView.as_view()),
url('', include(router.urls))
]
What could possibly be the problem? And the possible work-around?
If your code is pasted correctly, it looks like your HelloViewSet is inside your HelloApiView.
Indentation is important in Python. You need to unindent this code:
class HelloViewSet(viewsets.ViewSet):
'''Test API Viewset'''
def list(self, request):
'''Return Hello Message'''
a_viewset = [
'Uses Actions (list, create, retrieve, update, partial_update)',
'automatically maps to URLs using Router',
'More functionality with less code'
]
return Response({'message': 'Hello', 'a_viewset': a_viewset})
This will put it directly in your views module so it can be imported.
I'm forced to write some messy work, I need to create partial_update view, however it must use POST method, because the software in the end does not use PUT/PATCH methods.
Here are some assumptions:
everything needs to be routed via DefaultRouter() in urls.py - that's
why I'm using GenericViewSet
must use POST method for updating one field - that's why I'm overwriting post() method of UpdateModelMixin
instance.visible is a Boolean, which state is set to True as soon as the body is not empty.
Update works, except the permission_classess which are ignored. It totally does not check the request for valid credentials. I think it's because I totally overwrote the post(), right? How do I force authentication check within the post method?
urls.py:
from django.urls import include, path
from rest_framework import routers
from browse.views import *
router = routers.DefaultRouter()
[...]
router.register(r'update-article', UpdateArticleBodyViewSet)
urlpatterns = [
path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'),)
]
views.py:
class UpdateArticleBodyViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticated, )
def post(self, request, pk):
instance = get_object_or_404(Article, pk=pk)
instance.body = request.data.get("body")
if instance.body:
instance.visible = True
instance.save()
serializer = self.get_serializer(instance=instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
UPDATE
I've changed the code after the first question, now it looks like this:
views.py:
class UpdateArticleBodyViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticated, )
def partial_update(self, request, *args, **kwargs):
instance = self.queryset.get(pk=kwargs.get('pk'))
instance.body = request.data.get("body")
if instance.body:
instance.visible = True
instance.save()
serializer = self.get_serializer(instance=instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
urls.py
articles_viewset = UpdateArticleBodyViewSet.as_view({
'post': 'update'
})
router = routers.DefaultRouter()
router.register(r'update-article', articles_viewset, basename="article")
Which results in following error:
AttributeError: 'function' object has no attribute 'get_extra_actions'
There is a couple questions about it on StackOverflow already, but none of them provide the answer. Is there any way I can use Router in this case or am I forced to write urls explicitly?
The problem here is that both classes you are inheriting from does not have a post method, so you are not actually overing it. So the method is out of the authentication scope.
There are a lot of different ways to accomplish this. The most simple way I can think of is to change the post action for your route. Something like:
articles_viewset = UpdateArticleBodyViewSet.as_view({
'post': 'update'
})
router.register(r'update-article', articles_viewset)
This way, you would be able to use UpdateMixin without any problem. You sould just to tweak the update method if necessary.
I am creating a simple rest api using django REST framework. I have successfully got the response by sending GET request to the api but since I want to send POST request, the django rest framework doesn't allow POST request by default.
As in image(below) only GET,HEAD, OPTIONS are allowed but not the POST request
The GET and POST methods inside of views.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from profiles_api import serializers
from rest_framework import status
# Create your views here.
class HelloApiView(APIView):
"""Test APIView"""
#Here we are telling django that the serializer class for this apiViewClass is serializer.HelloSerializer class
serializer_class = serializers.HelloSerializer
def get(self, request, format=None):
"""Retruns a list of APIViews features."""
an_apiview = [
'Uses HTTP methods as fucntion (get, post, patch, put, delete)',
'It is similar to a traditional Django view',
'Gives you the most of the control over your logic',
'Is mapped manually to URLs'
]
#The response must be as dictionary which will be shown in json as response
return Response({'message': 'Hello!', 'an_apiview': an_apiview})
def post(self,request):
"""Create a hello message with our name"""
serializer = serializer.HelloSerializer(data=request.data)
if serializer.is_valid():
name = serializer.data.get('name')
message = 'Hello! {0}'.format(name)
return Response({'message':message})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
How to allow POST requests in django REST framework?
The problem with the code was, you have added the def post() after the return statement.
To solve, just correct your indentation level as below,
class HelloApiView(APIView):
def get(self, request, format=None):
return Response()
def post(self, request):
return Response()