Django Rest Framework: two viewsets for one router - python

I want to have TWO DIFFERENT viewsets (for example, one implements only the GET method, the other implements only the POST method), but which will have the same url:
GET /tournament/ - returns concrete object of the model Tournament;
POST /tournament/ - create object of the model Tournament.
But it is important that they must have the same url /tournament/!
I trying something like this:
models.py
class Tournament(Model):
...
viewsets.py
class PublicTournamentEndpoint(
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet
):
queryset = Tournament.objects.all()
authentication_classes = [] # empty!
class PrivateTournamentEndpoint(
mixins.CreateModelMixin,
viewsets.GenericViewSet
):
queryset = Tournament.objects.all()
authentication_classes = ['SomeAuthBackend'] # not empty!
routers.py
class TournamentRouter(SimpleRouter):
routes = [
Route(
url=r'^{prefix}/tournament/$',
mapping={
'get': 'retrieve',
'post': 'create',
},
name='{basename}',
detail=True,
initkwargs={},
),
urls.py
tournament_router = TournamentRouter()
tournament_router.register(
'tournaments',
PublicTournamentEndpoint,
basename='tournaments',
)
tournament_router.register(
'tournaments',
PrivateTournamentEndpoint,
basename='tournaments',
)
urlpatterns += tournament_router.urls
But my urlpatterns has next values:
[
<URLPattern '^tournaments/tournament/$' [name='tournaments']>,
<URLPattern '^tournaments/tournament/$' [name='tournaments']>
]
and so when I send a POST /tournament/ request, I get the following error:
405 "Method \"POST\" not allowed."
because the first match url does not have a POST method, but only GET. How can i resolve this problems?
Thank you!

You can't call 2 views for 1 url. Seem like you only want to allow specific user who has SomeAuthBackend permission can create new Tournament, if so, you could custom your permission class a bit to check permission only on POST requests like so:
from rest_framework import permissions
class SomeAuthBackend(permissions.BasePermission):
protected_methods = ['POST',]
def has_permission(self, request, view):
if request.method in self.protected_methods:
# Check permission here
return True
class TournamentEndpoint(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet
):
queryset = Tournament.objects.all()
authentication_classes = [SomeAuthBackend, ] # empty!
With this, other method still works but when user send a POST request, it require to pass the permission check on SomeAuthBackend.

Related

How to add suffix url in Django Rest Framework?

How to add suffix url in ModelViewSet
Serializer
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
def update(self, instance, validated_data):
...
...
ModelViewSet
I'm doing a custom partial update
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
http_method_names = ['get', 'patch', 'head', 'options']
def partial_update(self, request, *args, **kwargs):
super(CommentViewSet, self).partial_update(
request, *args, **kwargs)
return Response({
"data": request.data,
...
...
})
Urls
router = routers.DefaultRouter()
router.register(
"comments",
CommentViewSet
)
urlpatterns = [
path('api/v1/', include(router.urls))
]
Currently have this, but I want to add a suffix
url: http://localhost:8000/api/v1/comments/{id}
I want to do something like this
url: http://localhost:8000/api/v1/comments/{id}/update_or_whatever
What you want to do does not follow the REST architecture and popular practice. In REST, each endpoint represents a resource. The actions on the resource are represented by HTTP methods. So if you have the comments resource accessible through this url http://localhost:8000/api/v1/comments/, you can create (POST), get the list (GET) on the list endpoint and edit(PUT and PATCH), fetch a single comment (GET) and delete(DELETE) using the detail endpoint. In this way, you don't need to explicitly name the URL according to the action like http://localhost:8000/api/v1/comments/{id}/update. This is the architecture that DRF is built on and hence why you have this url style. Of course, there are actions like login and others that may not fit into this architecture and that's why DRF provides custom actions. But you should not use it to override the default actions mapped to HTTP methods
Another magic from DFR
https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions
Only change what u need in the view and add this action decorator.
In your views.py
#action(methods=['get'], detail=True, permission_classes=[IsAuthenticated])
def get_file(self, request, pk=None):
if pk is None:
raise ValueError("Found empty filename")
obj = self.get_queryset().filter(pk=pk).first()
if obj and obj.image_file:
return FileResponse(obj.image_file, content_type="image/jpeg")
return Response(
'Nothing to show',
status=status.HTTP_400_BAD_REQUEST)

DRF Post to ViewSet without writing into Model

I've always written data into database when posting via Django Rest Framework endpoints. This time I would like to process received data and send it somewhere else without writing into DB. I switched from ModelViewSet to ViewSet, I can issue GET request OK but receiving Bad Request 400 when I curl or POST via DRF URL. Here's a working minimal code (removed need for authentication etc):
urls.py
from django.urls import path, include
from .views import ContactView
from rest_framework import routers
router = routers.DefaultRouter()
router.register('message', ContactView, basename='message')
urlpatterns = [
path('', include(router.urls)),
]
serializers.py
from rest_framework import serializers
class ContactSerializer(serializers.Serializer):
text = serializers.CharField(max_length=250)
views.py
from rest_framework.response import Response
from .serializers import ContactSerializer
from rest_framework import viewsets
class ContactView(viewsets.ViewSet):
def list(self, request):
return Response('Got it')
def create(self, request):
serializer = ContactSerializer(data=request.data)
if serializer.is_valid():
return Response(serializer.data)
else:
return Response('Invalid')
Would greatly appreciate your suggestions.
You can use GenericAPIView for get or post request and do some logic in validate method, for example do something with signals or edit something. Also u can use #detailt_route or #list_route for any ModelViewSet for write special url for instance, example for edit extra data.
how i did rarely:
in urls.py
urlpatterns = [
url('v1/message', ContactAPIView.as_view(), name='message'),
]
in view.py
class ContactAPIView(GenericAPIView):
serializer_class = ContactSerializer
permission_classes = ()
def post(self, request, *args, **kwargs):
serializer_class = self.get_serializer_class()
serializer = serializer_class(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
data = {"status": True}
return Response(data)
in serializers.py
class ContactSerializer(serializers.Serializer):
text = serializers.TextField()
def validate(self, attrs):
write some logic
you are getting this error because you are using Viewsets which uses DefaultRouter to register routers for you. What it does is that it creates 2 urls for your viewset
message
message/id
so in your current case; i.e. viewset you need to send some dummy number in your url to access this post function (which is not a good approach).
So, you should use any class which parent doesn't include ViewSetMixin (which gives functionality of Router Registration) like in your case inherit your view from these classes
ListModelMixin
CreateModelMixin
GenericAPIView

Return custom response to successful POST request in django rest framework

I want to return a custom response to the user when they hit the API with a POST request and it's a success. Here are the code snippets :
views.py
class BlogPostAPIView(mixins.CreateModelMixin,generics.ListAPIView):
# lookup_field = 'pk'
serializer_class = BlogPostSerializer
def get_queryset(self):
return BlogPost.objects.all()
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def post(self,request,*args,**kwargs):
return self.create(request,*args,**kwargs)
urls.py
app_name = 'postings'
urlpatterns = [
re_path('^$', BlogPostAPIView.as_view(),name='post-create'),
re_path('^(?P<pk>\d+)/$', BlogPostRudView.as_view(),name='post-rud'),
]
Right now it's returning the details of the post request as successful response, is there any way I can return some other response based on my own custom queryset?
You can write custom api on views.py. I want to for example;
from rest_framework.views import APIView
from rest_framework.response import Response
class Hello(APIView):
#csrf_exempt
def post(self, request):
content = "Hi"
type = "message"
return Reponse({"content":content,"type":type})
and than define url.
app_name = 'postings'
urlpatterns = [
re_path('^$', BlogPostAPIView.as_view(),name='post-create'),
re_path('^(?P<pk>\d+)/$', BlogPostRudView.as_view(),name='post-rud'),
re_path('^hello/$', Hello.as_view(),name='Hello'),
]
That's it.
Also you can manage permessions : https://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy
and you can use serializer on views : https://www.django-rest-framework.org/api-guide/serializers/#saving-instances

Django Rest Framework: Proper retrieve view doesn't handle request when I slightly change parameter

I have this weird problem where when I try to retrieve one object from my database I receive the message {"detail":"Not found."}. I know the object is there because I can see it in my django admin. And when I try to retrieve another object it locates it fine.
urls.py
router = routers.DefaultRouter()
router.register(r'recalls', views.Recalls)
admin.autodiscover()
from rest_framework import generics, permissions, serializers
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include(router.urls)),
path('api/v1/recalls/rundate/<str:run_date>/', views.Recalls.as_view({'get': 'retrieve'}), name='retrieve_by_rundate'),
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),,
]
views.py
class Recalls(viewsets.ModelViewSet):
'''
This view will be fore retrieving a recall for a car from the database
'''
queryset = CarFax.objects.all()
serializer_class = RecallsSerializer
permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope]
def list(self, request, **kwargs):
queryset = GetRecalls.objects.all()
serializer = RecallsSerializer(queryset, many=True)
print('LIST')
return Response(serializer.data)
def retrieve(self, request, pk=None, *args, **kwargs):
queryset = CarFax.objects.all()
#record = get_list_or_404(queryset, self.kwargs)
record = get_list_or_404(queryset, vin__exact=pk)
serializer = RecallsSerializer(record, many=True)
print('RETRIEVE')
return Response(serializer.data)
def retrieve_by_rundate(self, request, run_date=None):
queryset = CarFax.objects.all()
#record = get_list_or_404(queryset, self.kwargs)
record = get_list_or_404(queryset, run_date__exact=run_date)
serializer = RecallsSerializer(record, many=True)
print('RETRIEVE RUNDATE')
return Response(serializer.data)
I am using the URL 'http://127.0.0.1:8000/api/v1/recalls/(?P<pk>[\w-]+)/$/' (which is automatically created by DefaultRouter(). The issue is that when I look up with one parameter, let's say test1 so
'http://127.0.0.1:8000/api/v1/recalls/test1' it works, and in my console I can see the message "RETRIEVE" from my "retrieve" method being printed. However, when I replace that with another object I created and I know exists, it isn't located and that "RETRIEVE" message isn't printed. So that view obviously isn't even being reached, why is that?
Thanks in advance

Django POST with ModelViewSet and ModelSerializer 405

How can I make a ModelViewSet accept the POST method to create an object? When I attempt to call the endpoint I get a 405 'Method "POST" not allowed.'.
Within views.py:
class AccountViewSet(viewsets.ModelViewSet):
"""An Account ModelViewSet."""
model = Account
serializer_class = AccountSerializer
queryset = Account.objects.all().order_by('name')
Within serializers.py:
class AccountSerializer(serializers.ModelSerializer):
name = serializers.CharField(required=False)
active_until = serializers.DateTimeField()
class Meta:
model = Account
fields = [
'name',
'active_until',
]
def create(self, validated_data):
with transaction.atomic():
Account.objects.create(**validated_data)
within urls.py:
from rest_framework import routers
router = routers.SimpleRouter()
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
Do I need to create a specific #action? my attempts to do so have yet to be successful. If that is the case what would the url = reverse('app:accounts-<NAME>') be such that I can call it from tests? I haven't found a full example (urls.py, views.py, serializers.py, and tests etc).
I discovered what the issue was, I had a conflicting route. There was a higher level endpoint registered before the AccountViewSet.
router.register(
prefix=r'v1/auth',
viewset=UserViewSet,
base_name='users',
)
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
Django runs through each URL pattern, in order, and stops at the first one that matches the requested URL.. I should have been ordered this way:
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
router.register(
prefix=r'v1/auth',
viewset=UserViewSet,
base_name='users',
)
despite the fact that reverse('appname:acccounts-list') worked, the underlying URL router still thought I was calling the UserViewSet.
From the docs:
A ViewSet class is simply a type of class-based View, that does not provide any method handlers such as .get() or .post(), and instead provides actions such as .list() and .create().
And here is a list of supported actions:
def list(self, request):
pass
def create(self, request):
pass
def retrieve(self, request, pk=None):
pass
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
def destroy(self, request, pk=None):
pass
So no post is not directly supported but create is.
So your end point would be v1/auth/accounts/create when using the a router instead v1/auth/accounts/post.
I honestly prefer using class based or function based views when working with DRF. It resembles regular django views more closely and makes more sense to me when working with them. You woul write you views and urls pretty much like regular django urls and views.

Categories