I expected React-admin to work out the box with Django Rest Framework, like its website implied but my experience is that it doesn't. It has been a time consuming task trying to set custom headers to fit react-admins requirement for X-Total-Count header for every response. Django Rest Framework prefers to put the count in to the json response it seems.
Does anyone know how to read this information from the json instead? It seems logical to me to set an option in react admin instead of rewriting the middleware with Django or other rest frameworks.
Here is a solution example using Class based Views.
example of views.py:
from .models import AzureADList
from rest_framework import pagination
from rest_framework.response import Response
from .serializers import AzureADListSerializer
from rest_framework import generics
def _positive_int(integer_string, strict=False, cutoff=None):
"""
Cast a string to a strictly positive integer.
"""
ret = int(integer_string)
if ret < 0 or (ret == 0 and strict):
raise ValueError()
if cutoff:
return min(ret, cutoff)
return ret
class StandardResultsSetPagination(pagination.LimitOffsetPagination):
'''This is to handle react-admins call to our API when paginating'''
offset_query_param = '_start'
def get_paginated_response(self, data):
headers={'X-Total-Count': self.count}
response = Response(data, headers=headers)
return response
def get_limit(self, request):
print('request query params..')
print(request.query_params)
try:
end = request.query_params['_end']
start = request.query_params['_start']
limit = int(end) - int(start)
return _positive_int(limit)
except (KeyError, ValueError):
pass
return self.default_limit
class UserViewSet(generics.ListCreateAPIView):
queryset = AzureADList.objects.all()
serializer_class = AzureADListSerializer
pagination_class = StandardResultsSetPagination
def get(self, request, *args, **kwargs):
queryset = self.get_queryset()
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page, many=True)
response = self.get_paginated_response(serializer.data)
return response
Related
I am trying to implement an api version of a play button on a django website.
This is how far I got:
models.py
class Note(models.Model):
plays = models.ManyToManyField(settings.AUTH_USER_MODEL,blank=True,related_name='track_plays')
def get_play_url(self):
return "/play/{}/play".format(self.pk)
def get_api_like_url(self):
return "/play/{}/play-api-toggle".format(self.pk)
views.py
class TrackPlayToggle(RedirectView):
def get_redirect_url(self,*args,**kwargs):
id = self.kwargs.get("id")
obj = get_object_or_404(Note,id=id)
url_ = obj.get_absolute_url()
user = self.request.user
if user.is_authenticated():
if user in obj.plays.all():
obj.plays.add(user)
else:
obj.plays.add(user)
return url_
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication,permissions
from rest_framework.decorators import api_view
class TrackPlayAPIToggle(RedirectView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
#api_view(['GET', 'POST', ])
def get(self,request,format=None):
obj = get_object_or_404(Note,id=id)
url_ = obj.get_absolute_url()
user = self.request.user
updated = False
played = False
if user.is_authenticated():
if user in obj.plays.all():
played = True
obj.plays.add(user)
else:
played = True
obj.plays.add(user)
played = False
updated = True
data = {
"updated":updated,
"played":played
}
return Response(data)
urls.py
url(r'^(?P<id>\d+)/play/', TrackPlayToggle.as_view(), name='play-toggle'),
url(r'^api/(?P<id>\d+)/play/', TrackPlayAPIToggle.as_view(), name='play-api-toggle'),
Ive added the API Decorator, because without it, I get a TypeError:
get() got an unexpected keyword argument 'id'
and when I try to add id=None I get an AssertionError:
.accepted_renderer not set on Response
Is this because I used id instead of slug?
Thank you for any suggestions
I don't understand why you thought adding the #api_view decorator would solve your TypeError. That decorator is for function-based views; it has no use in class-based views, where you define which methods are supported by simply defining the relevant methods. Remove the decorator.
The way to solve the original problem is to add the id parameter to the method; and the way to solve the problem with the renderer is to inherit from the correct parent class, which should clearly not be RedirectView.
class TrackPlayAPIToggle(GenericAPIView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, id, format=None):
...
I have a very basic Django Rest API.
I don't know how to have some HTML views, in the same django project, which uses API (finally keeping API returning JSON only).
I followed this, but it seems to change the API View (in this case, curl will retrieve HTML and not JSON) :
https://www.django-rest-framework.org/api-guide/renderers/#templatehtmlrenderer
Do I need another Django App ? Another Django project ? Some JS ?
EDIT :
Ok, I've seen it's possible, thanks to rrebase.
But I can't retrieve the JSON with Curl, here my views.py
from rest_framework import generics
from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAdminUser
from . import models
from . import serializers
class UserListView(generics.ListAPIView):
renderer_classes = [JSONRenderer, TemplateHTMLRenderer]
template_name = 'profile_list.html'
def get(self, request):
queryset = models.CustomUser.objects.all()
serializer_class = serializers.UserSerializer
return Response({'profiles': queryset})
My models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
def __str__(self):
return self.email
I get an error "Object of type 'CustomUser' is not JSON serializable" when I request the API (http://127.0.0.1:8000/api/v1/users/)
Sorry, it's some different that initial question...
Yes, you can have both. The link you provided to docs has the following:
You can use TemplateHTMLRenderer either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
When making an API request, set the ACCEPT request-header accordingly to html or json.
Finally I made some conditions in my view, and it's working
class UserListView(generics.ListAPIView):
renderer_classes = [JSONRenderer, TemplateHTMLRenderer]
permission_classes = (IsAdminUser,)
def get(self, request):
queryset = CustomUser.objects.all()
if request.accepted_renderer.format == 'html':
data = {'profiles': queryset}
return Response(data, template_name='profile_list.html')
else:
serializer = UserSerializer(queryset, many=True)
data = serializer.data
return Response(data)
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()
I try to test view that has custom authentication, mainly because the main auth is based on external login-logout system, utilizing Redis as db for storing sessions.
Auth class is checking session id from the request, whether it is the same as in Redis - if yes, succeed.
My custom authentication.py looks like:
from django.utils.six import BytesIO
from rest_framework import authentication
from rest_framework import exceptions
from rest_framework.parsers import JSONParser
import redis
class RedisAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
print(request.META)
token = request.META['HTTP_X_AUTH_TOKEN']
redis_host = "REDIS_IP_ADRESS"
redis_db = redis.StrictRedis(host=redis_host)
user_data = redis_db.get("user_feature:{}".format(token))
if user_data is None:
raise exceptions.AuthenticationFailed('No such user or session expired')
try:
stream = BytesIO(user_data) # Decode byte type
data = JSONParser(stream) # Parse bytes class and return dict
current_user_id = data['currentUserId']
request.session['user_id'] = current_user_id
except Exception as e:
print(e)
return (user_data, None)
and my views.py looks like:
#api_view(['GET', 'POST'])
#authentication_classes((RedisAuthentication, ))
def task_list(request):
if request.method == 'GET':
paginator = PageNumberPagination()
task_list = Task.objects.all()
result_page = paginator.paginate_queryset(task_list, request)
serializer = TaskSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
elif request.method == 'POST':
serializer = PostTaskSerializer(data=request.data)
if serializer.is_valid():
user_id = request.session.get('user_id')
serializer.save(owner_id=user_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Manual tests pass, but my current pytests failed after adding authentication.py, and have no clue how I can fix it properly - tried with forcing auth, but no succeed.
I'm thinking that one of solution will be use fakeredis for simulate real redis. Question is, how that kind of test should looks like?
Example of test you could find here:
#pytest.mark.webtest
class TestListView(TestCase):
def setUp(self):
self.client = APIClient()
def test_view_url_accessible_by_name(self):
response = self.client.get(
reverse('task_list')
)
assert response.status_code == status.HTTP_200_OK
#pytest.mark.webtest
class TestCreateTask(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='admin', email='xx', password='xx')
def test_create(self):
data = {some_data}
self.client.login(username='xx', password='xx')
response = self.client.post(
reverse('task_list'),
data,
format='json')
assert response.status_code == status.HTTP_201_CREATED
self.client.logout()
Thanks in advance for any help!
I managed to mock whole redis auth using mock.patch decorator - https://docs.python.org/3.5/library/unittest.mock-examples.html#patch-decorators.
When you put import patch to mock.patch decorator, do not insert absolute module path where redis code is stored, but insert the path where redis code was imported as a module and used.
My test looks like that now:
#mock.patch('api.views.RedisAuthentication.authenticate')
def test_view_url_accessible_by_name(self, mock_redis_auth):
data = {"foo": 1, "currentUserId": 2, "bar": 3}
mock_redis_auth.return_value = (data, None)
response = self.client.get(
reverse('task_list'),
HTTP_X_AUTH_TOKEN='foo'
)
assert response.status_code == status.HTTP_200_OK
I've got a Django Rest Framework ModelViewSet and am trying to use the TemplateHTMLRenderer to display HTML. Following along in the tutorial:
from rest_framework import permissions, renderers, viewsets
from rest_framework.decorators import link
from . import models, serializers
from .permissions import IsOwnerOrReadOnly
class SnippetViewSet(viewsets.ModelViewSet):
template_name = 'snippet-list.html'
queryset = models.Snippet.objects.all()
serializer_class = serializers.SnippetSerializer
renderer_classes = (renderers.TemplateHTMLRenderer,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
#link(renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
def pre_save(self, obj):
obj.owner = self.request.user
If I add a key in def resolve_context() I can access the model objects in my template that are passed into the RequestContext. If I don't add the data key then I don't know how to access the Snippets.
def resolve_context(self, data, request, response):
if response.exception:
data['status_code'] = response.status_code
#return RequestContext(request, data) # original source on github
return RequestContext(request, {'data': data}) # if I add a key I can access it
So I've got to be missing something easy or how I'm expecting this to behave is not how the authors intended?
I would go this way:
class SnippetViewSet(viewsets.ModelViewSet):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
renderer_classes = (renderers.JSONRenderer, renderers.TemplateHTMLRenderer)
def list(self, request, *args, **kwargs):
response = super(SnippetViewSet, self).list(request, *args, **kwargs)
if request.accepted_renderer.format == 'html':
return Response({'data': response.data}, template_name='home.html')
return response
and use http://127.0.0.1:8000/snippets/.html to get table (or whatever suffix you use).
This way you don't override resolver for each render type.
Other solution would be to just create dedicated view for list action and only use HTML renderer. But then you would have a small code duplication.
I also met the same question with you, and I also thought so. I came here by Google. I didn't like override "def list(self, request, *args, **kwargs):", because I felt it broke the viewset design idea. After I researched the snippet tutorial and source code in the "site-packages\rest_framework", I got the key, not viewset but "serializer.data". In the "site-packages\rest_framework\serializers.py", I found the class BaseSerializer, i.e., the top base class of ModelSerializer. Its property "data" is defined as follows:
#property
def data(self):
... # omitted the function body here, because it didn't care about this solution.
return self._data
This property data is just the "serializer.data" that is just the response passed to template. So I just overrided the data property in "snippets/serializers.py", and after calling the father's method, set the key for the returned data:
class SnippetSerializer(serializers.ModelSerializer):
#property
def data(self):
return { 'data' : super(serializers.ModelSerializer, self).data } #'data' can be replaced with other wanted name.
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
OK, use the name 'data' in your template.
I subclassed and overrode the method that provides the template context, so that the serializer data is available under data within the template context:
from rest_framework.renderers import TemplateHTMLRenderer
class MyHTMLRenderer(TemplateHTMLRenderer):
def get_template_context(self, data, renderer_context):
context = {'data': data}
response = renderer_context['response']
if response.exception:
data['status_code'] = response.status_code
return context
Inside the viewset use renderer class
renderer_classes = (renderers.JSONRenderer, renderers.TemplateHTMLRenderer)
like above and override the ListModelMixin's list method.
mariodev's answer gives the best example also.