Handle many ViewSets with one URL in the Django rest framework? - django

What I want is to handle three ViewSet with one URL respect to the provided request.data.
Let's make it more clear by diving into code:
Here is my router.py
# vim: ts=4 sw=4 et
from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter
from . import viewsets
router = DefaultRouter()
router.register(r'endpoint', viewsets.MyViewSet, 'my-viewset')
notification_urls = [
url(r'^', include(router.urls)),
]
Question1:
Is it possible to handle three ViewSet with one URL?
For example, according to the data that has been sent, I want to select dynamically a different ViewSet instead of assigned MyViewSet.
---> /endpoint/ ---- | request.data['platform'] == 1 ----> ViewSet1
| request.data['platform'] == 2 ----> ViewSet2
| request.data['platform'] == 3 ----> ViewSet3
and I know that I can call other ViewSet's methods in a MyViewSet something like this Question but this is really a messy one.
Question2:
Is it possible to dynamically pass extra params respect to the data that has been sent to a ViewSet?
For example, according to the data that has been sent, I want to pass an extra param into MyViewSet.
I know that I can do all this stuff in ViewSet initial method, Like this:
def initial(self, request, *args, **kwargs):
self.set_platform()
super(NotificationViewSet, self).initial(request, *args, **kwargs)
But what I want is to handle this stuff during the MyViewSet instantiation.
NOTE: Maybe I can handle this by Writing a Middleware but if there are other options I would really be appreciated for sharing them with me.

Related

Query parameters in request on Django 3.0.3

I'm putting together an API and need to add query parameters to the URI like https://www.example.com/api/endpoint?search=term&limit=10.
My first question is, in Django 3.0, I'd need to use re-path to accomplish parsing out the various parameters, correct?
Secondly, the question is about convention. It seems like two of the three APIs I've been working with a lot lately us a convention like:
/api/endpoint?paramater1=abc&parameter2=xyz
Another uses something like:
/api/endpoint?$parameter1=abc&parameter2=abc
Looking at some past Django question related to this topic, I see stuff like:
/api/endpoint/?parameter1=abc&parameter2=xyz
Another post I read was suggesting that parameters should be separated with ;.
I guess I'm just curious what the "correct" convention should be either in terms of Django or general concensus.
Lastly, it seems to me what I'm trying to accomplish should be a GET request. The front-end sends the user defined parameters (section and startingPage) to the back-end where a PDF is generated matching those parameters. When it is generated, it sends it back to the FE. The PDFs are much too large to generate client-side. GET would be the correct method in the case, correct?
Well, I elected to go with the first convetion:
/api/endpoint?paramater1=abc&parameter2=xyz
Simply because the majority of APIs I work with use this convention.
For my files:
# urls.py
from django.urls import path
from .views import GeneratePDFView
app_name = 'Results'
urlpatterns = [
path('/endpoint',
GeneratePDFView.as_view(), name='generate_pdf')
]
# view.py
from django.conf import settings
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST
from rest_framework.views import APIView
class GeneratePDFView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
if len(request.query_params) == 2:
data['id'] = {'id': request.user.id}
data['starting_page'] = request.query_params.get('parameter1')
data['data_view'] = request.query_params.get('parameter2')
serializer = GeneratePDFSerializer(data=data)
...

Django Rest Framework Custom Endpoints

I have recently inherited an API built with Django and DRF. I need to add some endpoints to the API but have never worked with Django or DRF before so I am trying to come up to speed as quickly as possible.
I am wondering how to do custom endpoints that don't just translate data too/from the backend database. A for instance might be an endpoint that reads data from the DB then compiles a report and returns it to the caller in JSON. But I suppose that right now the simplest method would be one that when the endpoint is hit just prints 'Hello World' to the log and returns a blank page.
I apologize if this seems basic. I've been reading through the docs and so far all I can see is stuff about serializers when what I really need is to be able to call a custom block of code.
Thanks.
if you want your REST endpoint to have all: GET, POST, PUT, DELETE etc. functionality then you have to register a route in your urls.py:
urls.py:
from rest_framework import routers
from django.urls import path, include
from . import views
router = routers.DefaultRouter()
router.register(r'hello', views.HelloWorldViewSet)
urlpatterns = [
# Wire up our API using automatic URL routing.
# rest_framework api routing
path('api/', include(router.urls)),
# This requires login for put/update while allowing get (read-only) for everyone.
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
now the url: /hello/ points to the HelloWorldViewSet.
in your views.py add the HelloWorldViewSet that will inherits from the rest_framework.viewsets.ViewSet class. You can override the ViewSet default class behavior by defining the following "actions": list(), create(), retrieve(), update(), partial_update(), destroy(). For displaying "hello world" on GET you only need to override list():
so in your views.py:
from rest_framework import viewsets
from rest_framework.response import Response
class HelloWorldViewSet(viewsets.ViewSet):
def list(self, response):
return Response('Hello World')
So, in your more advanced list() function you have to interact with the database, to retrieve the data you want, process it and create the report as a json serializable dictionary and return it as a Response object.
If you don't want to override the standard list action, you could instead add a new action to the HelloWorldViewSet let's call it report:
so in your views.py:
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.decorators import action
class HelloWorldViewSet(viewsets.ViewSet):
#action(detail=False)
def report(self, request, **kwargs):
return Response('Hello World')
I hope this is what you were looking for.
Note that you don't need django-rest-framework if you are not interested in POST, PUT, PATCH, DELETE, etc... you can simply add a path to your urls.py that points to a Django view function that returns a Django JsonResponse object containing your report.
One good option for you would be DRF actions. Docs here
The action allows you to choose a relevant view for what you wanna do, so you can just pop things into the API you inherited. No additional setup needed, they show up next to the regular routes.

How does routers and viewsets configure their urls?

I was reading through a long piece of code. And was stuck at how routers and viewsets automatically configure their URLs.
For eg.
the views.py file is:
class UserViewSet(viewsets.ModelViewSet):
authentication_classes = (BasicAuthentication,SessionAuthentication)
permission_classes = (IsAuthenticated,)
serializer_class = UserSerializer
queryset = User.objects.all()
The corresponding urls with router is:
router = DefaultRouter()
router.register(r'users',views.UserViewSet,basename='user')
urlpatterns = router.urls
In the above case, what will be the respective urls for the different actions in viewsets, ie list, create, retrieve, update, partial_update and destroy as mentioned in the djangorestframework documentation on viewsets: http://www.tomchristie.com/rest-framework-2-docs/api-guide/viewsets
When you register the viewset it will generate the following url patterns for above case.
router.register(prefix='users', viewset=views.UserViewSet, basename='user')
It follows the below regex patterns
# Regex for list
r'^{prefix}{trailing_slash}$'
# Regex for detail
r'^{prefix}/{lookup}{trailing_slash}$'
1. List router allows http methods like get to retrieve the resource and post to create the resource.
2. Detail router allows http methods like get to retrieve the data of a a resource,put to update the data of a resource, patch to partial update of resource and delete to delete the resource.
We can also pass a extra keyword argument format while using reverse to generate the dynamic url.
URL patterns for above case
[<URLPattern '^users/$' [name='user-list']>,
<URLPattern '^users\.(?P<format>[a-z0-9]+)/?$' [name='user-list']>,
<URLPattern '^users/(?P<pk>[^/.]+)/$' [name='user-detail']>,
<URLPattern '^users/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$' [name='user-detail']>,
<URLPattern '^$' [name='api-root']>,
<URLPattern '^\.(?P<format>[a-z0-9]+)/?$' [name='api-root']>]
Reference: https://github.com/encode/django-rest-framework/blob/master/rest_framework/routers.py

Django-Rest-Framework router register

i'm having a issue when i try to register more than 2 routers using Django-REST-FRAMEWORK. Please take a look on my example:
urls.py
from rest_framework import routers
from collaborativeAPP import views
router = routers.DefaultRouter()
router.register(r'get_vocab', views.VocabViewSet)
router.register(r'get_term', views.TermViewSet)
router.register(r'get_discipline', views.DisciplineViewSet)
urlpatterns = patterns(
...
url(r'^service/', include(router.urls))
)
views.py
class VocabViewSet(viewsets.ModelViewSet):
queryset = Vocab.objects.all()
serializer_class = VocabSerializer
class TermViewSet(viewsets.ModelViewSet):
queryset = Term.objects.all()
serializer_class = TermSerializer
class DisciplineViewSet(viewsets.ModelViewSet):
queryset = Vocab.objects.filter(kwdGroup=4)
serializer_class = DisciplineSerializer
the result in my localhost is the following:
http://localhost:8000/service/
HTTP 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, HEAD, OPTIONS
{
"get_vocab": "http://127.0.0.1:8000/service/get_discipline/",
"get_term": "http://127.0.0.1:8000/service/get_term/",
"get_discipline": "http://127.0.0.1:8000/service/get_discipline/"
}
As you can see i have registered 3 routers expecting that they will display 3 urls for each methodname(get_vocab, get_term, get_discipline). The final result is get_discipline is occuring two times and get_vocab url is missing.
Notice that for methods that uses different models it works fine, but in case of get_discipline and get_vocab they use the same model which will create this mess. Should i use a viewset for each model? If so, how can a define different methods in a viewset?
It should occur the following result:
HTTP 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, HEAD, OPTIONS
{
"get_vocab": "http://127.0.0.1:8000/service/get_vocab/",
"get_term": "http://127.0.0.1:8000/service/get_term/",
"get_discipline": "http://127.0.0.1:8000/service/get_discipline/"
}
What am i missing? I supposed that i could register as many routers as i want. It is supposed to have one router per model? Why doesn't seem to work for viewsets that share a same model?
Try explicitly adding a base_name to each registered viewset:
router = routers.DefaultRouter()
router.register(r'vocabs', views.VocabViewSet, 'vocabs')
router.register(r'terms', views.TermViewSet, 'terms')
router.register(r'disciplines', views.DisciplineViewSet, 'disciplines')
As a side note, your should probably exclude get_ prefix in your urls since that is not RESTful. Each URL should specify a resource, not an action on the resource. Thats what HTTP verbs are used for:
GET http://127.0.0.1:8000/service/vocabs/
# or this to create resource
POST http://127.0.0.1:8000/service/vocabs/
...
Here some more information about router:
router = routers.SimpleRouter()
router.register(r'users', UserViewSet)
router.register(r'accounts', AccountViewSet)
urlpatterns = router.urls
Here exist two mandatory arguments for register() method:
prefix : The URL prefix to use for this set of routes.
viewset : The viewset class.
And what abut base_name?
The base to use for the URL names that are created. if you don't set the base_name, it will be automatically generated according to the model or queryset attribute on the viewset.
Here is the URL patterns generated by example above:
URL pattern: ^users/$ Name: 'user-list'
URL pattern: ^users/{pk}/$ Name: 'user-detail'
URL pattern: ^accounts/$ Name: 'account-list'
URL pattern: ^accounts/{pk}/$ Name: 'account-detail'
Now if you want create custome route you should write methods on the viewset decorated with #link or #action like this:
#action(permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
The following URL generated:
URL pattern: ^users/{pk}/set_password/$ Name: 'user-set-password'
You used the DefaultRouter and this router is similar to SimpleRouter, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional .json style format suffixes.
See this picture of detail table
In previous answer you got the right answer .. me just have given you some more information about router
I am not sure if this is an answer or comment (or a question).
In my case I have two endpoints to a single model and #miki725 answer was not help for me. I was forced to use separated views and separated serializers.
In urls.py I have the 3rd parameter (like #miki725 suggests)
router.register(r'doc_v1', views.DocV1ViewSet, basename='doc_v1')
router.register(r'doc_v2', views.DocV2ViewSet, basename='doc_v2')
But this is not enough. I must use a separated serializers and connect them to the proper detail view via explicitly defined url field:
url = serializers.HyperlinkedIdentityField(view_name='doc_v1-detail')
This is a pain, because (sometimes) both views differ in really minor details like access permissions are. And I have to make almost identical duplicates of view and serializer.
Not good behavior but I have no other solution for now.

JWT authentication doesn't work for custom controller in Django

I am using the Django Rest Framework in my Python app, and am using JSON Web Token Authentication (DRF JWT) for the api authentication.
My problem comes when I am building a custom controller. I pointed a specific URL to a function in my calculations.py file that I created. Following are how they look.
urls.py
from django.conf.urls import patterns, include, url
from django.contrib import admin
from rest_framework import routers
from app.serializers import xxxViewSet, yyyViewSet
from app.calculations import getReturns
router = routers.DefaultRouter()
router.register(r"xxx", xxxViewSet)
router.register(r"yyy", yyyViewSet)
urlpatterns = patterns('',
url(r'^admin/', include(admin.site.urls)),
url(r'^api/auth/token/$', 'rest_framework_jwt.views.obtain_jwt_token'),
url(r'^api/auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^api-token-verify/', 'rest_framework_jwt.views.verify_jwt_token'),
url(r'^api/', include(router.urls)),
**url(r'^getReturns/', getReturns),**
)
calculations.py
from django.http import HttpResponse
from .models import xxx, yyy, zzz, aaa
def getReturns(request):
data = request.GET('data')
**running calculations here on data and giving out response**
return HttpResponse(response)
serializers.py
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework import routers, serializers, viewsets, permissions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from .models import xxx, yyy, zzz, aaa
class xxxSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = xxx
fields = ('id', 'name')
class xxxViewSet(viewsets.ModelViewSet):
authentication_classes = [SessionAuthentication, BasicAuthentication, JSONWebTokenAuthentication]
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
queryset = xxx.objects.all()
serializer_class = xxxSerializer
The above serializers.py file contains serializer classes for all my models, and also viewsets for the same. I haven't yet transferred the viewsets into views.py, so that file is empty for now.
Anyway, my calculations.pyis separate from these files, and the function defined in this file is directly being called by the '/getReturns/' URL without going through a view. How do I incorporate the functions defined in my calculations file into a viewset so that my authorization classes are called before the function gets executed?
I started doing this in comments and it was too long. Generally, you haven't really provided enough code to assist properly, but here's my crack anyway. It's not obvious which version/implementation of Django JWT you're using (there are a few), how you're authorising your views, or whether your calculations.py file is a view or something else. (If it's something else, I'd authorise in the view, and call it from there.)
Why are you unable to send a POST? Generally, Once you have the token in your front end, you can use from rest_framework.decorators import authentication_classes and #authentication_classes([JSONWebTokenAuthentication,]) wrapper on any function that needs authorisation.
That looks like this:
#authentication_classes([JSONWebTokenAuthentication,])
def function_here(arguments):
#function does stuff
How are you passing/trying to send the web token back to the application?
Presumably in CURL your initial auth to get a token looks something like:
curl -X POST -d "username=admin&password=abc123
after that you get (if you're using rest_framework_jwt) the token back:
{JWTAuthorization: YourTokenHere}.
After that, to return it to DRF protected pages (assuming they're wrapped, as above, or have a similar protection) - you haven't outlined how you're authorisation - then from the docs you do:
curl -H "Authorization: JWT <your_token>" http://localhost:8000/protected-url/
If you're generating the call in Angular or similar, then it's the same - you need to pass it in the headers.
Edit: I'd also note you've drastically increased the amount of code here since my original answer. Fundamentally though, you need to check the auth classes you're declaring; the easiest way is as specified above.