I'm using REST in Django, And I couldn't understand what is the main difference between classic URL and instantiating DefaultRouter() for registering URL by ViewSet.
I have a model:
class Article(models.Model):
title = models.CharField()
body = models.TextField()
author = models.ForeignKey()
Serializing model like this:
from blog.models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['title', 'body', 'author']
View Class:
from blog.models import Article
from rest_framework import viewsets
from .serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
queryset = Article.objects.all()
and URLS:
router = DefaultRouter()
router.register(r'articles', ArticleViewSet)
urlpatterns = [
path('', include(router.urls)),
]
Is it possible to use classic URL in URLS.py instead of instantiating the object for a ViewSet like this:
urlpatterns = [
path('api/', 'views.someAPI'),
]
I just know HTTP method in ViewSet translate methods to retrieve, list and etc...
The Question is can we use traditional(Classic) URL style in this situation, Should we ?
Thanks for your help.
Well, in a nutshell as a django developer it is notorious how it is hard to deal with normal urls in django in some cases. Every now and again we get confused with the id type of the detail page that in some case are strings or integers with its regex, and so on.
For example:
urlpatterns = [
url(r'^(?P<content_type_name>[a-zA-z-_]+)$', views.content_type, name = 'content_type'),
]
# or
urlpatterns = [
url(r'^(?P<content_type_name>comics|articles|videos)$', views.content_type, name='content_type'),
]
Not mentioning that in almost every case its needed to have two urls like:
URL pattern: ^users/$ Name: 'user-list'
URL pattern: ^users/{pk}/$ Name: 'user-detail'
THE MAIN DIFFERENCE
However, using DRF routers the example above is done automatically:
# using routers -- myapp/urls.py
router.register(r"store", StoreViewSet, basename="store")
How django will understand it:
^store/$ [name='store-list']
^store\.(?P<format>[a-z0-9]+)/?$ [name='store-list']
^store/(?P<pk>[^/.]+)/$ [name='store-detail']
^store/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$ [name='store-detail']
See how much job and headache you have saved with a line of code only?
To contrast, according to DRF documentation the routers is a type of standard to make it easy to declare urls. A pattern brought from ruby-on-rails.
Here is what the documentation details:
Resource routing allows you to quickly declare all of the common
routes for a given resourceful controller. Instead of declaring
separate routes for your index... a resourceful route declares them in
a single line of code.
— Ruby on Rails Documentation
Django rest framework documentation:
Some Web frameworks such as Rails provide functionality for
automatically determining how the URLs for an application should be
mapped to the logic that deals with handling incoming requests.
REST framework adds support for automatic URL routing to Django, and
provides you with a simple, quick and consistent way of wiring your
view logic to a set of URLs.
For more details follow the django rest framework documentation.
Related
I have an Comment which have an Article foreign key (so Article have an "array" of comments). I need to build an url to fetch theese comments using article's pk, but when I am trying to do smth like "articles/int:article_pk/comments/" or "articles/{article_pk}/comments/" drf router crates static route with path "articles/{article_pk}/comments/". How can I implement getting a comments using article pk?
urls.py
router = DefaultRouter()
router.register('articles', articleAPI.ArticleAPI, basename='articles')
router.register('articles/comments', CommentAPI, basename='comments')
You can use this same url to get comments as well.
router.register('articles', articleAPI.ArticleAPI, basename='articles')
add a new method to Article ModelViewSet
#action(methods=['get'], detail=True,
url_path='comments', url_name='article-comments')
article = self.get_object()
serializer = CommentSerializer(queryset=article.comment_set.all(), many=True) # comment_set resembles the related name for article foreign key
return Response(serializer.data)
In postman hit the url articles/<article_id>/comments/ with GET method to get the comment list
Its not working because that is not how routers are intended to be used. You don't specify the key when registering, you use the viewset to define it. Read the docs at [1].
[1]: https://www.django-rest-framework.org/api-guide/routers/#simplerouter
A router can indeed not do that. You will need to capture the article_pk in the path and then work with two routers, so:
article_router = DefaultRouter()
article_router.register('articles', articleAPI.ArticleAPI, basename='articles')
comment_router = DefaultRouter()
comment_router.register('comments', CommentAPI, basename='comments')
urlpatterns = [
path('', include(article_router.urls)),
path('articles/<int:article_pk>/', include(comment_router.urls)),
]
I'm having an issue with a new django 4 app not processing or not allowing POST method. I used to bypass this in Django version 3 by adding a trailing slash "/" to the end of url in Postman API Tester like http://127.0.0.1:8000/api/didipay/ instead of http://127.0.0.1:8000/api/didipay . But now in my Django 4 API I completed the url with the slash but POST method is still not processing the data. It gives me a "500 internal server error" and I don't understand where it is coming from. The GET method is rather giving an empty array which is normal because I've not inserted any data yet in the database. I'm using venv, Django 4 Rest framework, serializer, viewSet and these is my models and API urls configurations:
//Serialize.py
from rest_framework.decorators import permission_classes
from didipay_app.models import Didipay_app
from rest_framework import viewsets, permissions
from .serializers import Didipay_appSerializer
class Didipay_appViewSet(viewsets.ModelViewSet):
queryset = Didipay_app.objects.all()
permission_classes = [
permissions.AllowAny
]
serializer_class = Didipay_appSerializer
// Project folder's urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('', include('didipay_app.urls')),
]
// App folder's urls.py
from rest_framework import routers, urlpatterns
from .api import Didipay_appViewSet
router = routers.DefaultRouter()
router.register('api/didipay', Didipay_appViewSet, 'didipay')
urlpatterns = router.urls
The application's web page says:
HTTP 200 OK Allow: GET, HEAD, OPTIONS Content-Type: application/json
Vary: Accept
{
"api/didipay": "http://127.0.0.1:8000/api/didipay/" }
And POST is even missing from the allowed method, and I don't understand why, since I granted all permissions in the Serialize.py file (No authentication systhem yet).
I will be very greatful for your help regarding this issue. Thank you in advance.
I got it fixed by changing fields = '__ all __' in the Model by all the table attributes in fields = ('id', 'first_name', 'last_name', 'phone', 'email', 'country', 'currency', 'status', 'payingType') because restarting the computer made console erroring out this
TypeError: The fields option must be a list or tuple or "__ all __"
which was not shown before restarting the system. I also suspect having set fields = '__ all __' instead of fields = '__ all __' (there is an issue about using __ in stackoverflow editor because it is hidden when used like in real code editor, but the overall idea is that there should not be spaces around "all" when using "__".
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.
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.
I have a RoutingUrl model which describes all the urls used on my site with the view (foreign key to the View model) that has to manage the url and some other routing information. The urls are continuously growing in size, and should also support the redirect. The models are more or less the following:
class RoutingUrl(models.Model):
url = models.CharField(unique=True, verbose_name='routing url')
crc_checksum = models.IntegerField(editable=False)
redirect_to = models.ForeignKey('RoutingUrl', related_name='redirect_from', blank=True, null=True)
view = models.ForeignKey(View, related_name='routing_urls')
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
class View(models.Model):
name = models.CharField()
The RoutingUrl has also a generic foreign key with the model containing information about the page to be rendered (different models has to be supported, this is the reason for the generic foreign key).
Now the question is: how to implement such a dynamic routing into django ? My feeling is that I have two options:
I could create a middleware which will take care of dispatching the request to the right view by implementing the process_request method (and so before the urlpatterns are checked). Obviously such a middleware should be positioned at the bottom of the middleware stack, in order to maintain the functionality of other middlewares. This solution will thus bypass the Django routing system.
Another option could be to add a single catch all urlpattern that matches everything, and then just write my own handler/dispatcher as a view. That handler implement the routing process and so will call the appropriate view and return its HttpResponse
Could you suggest me which of the two options is the best to implement such a routing ? Of course if there is a third, better option, don't hesitate to suggest it to me.
Investigating a bit into the django middleware hooks, it become evident that the process_request is not the best candidate for implementing the routing functionality. Indeed, from the documentation:
It should return either None or an HttpResponse object. If it returns
None, Django will continue processing this request, executing any
other process_request() middleware, then, process_view() middleware,
and finally, the appropriate view. If it returns an HttpResponse
object, Django won’t bother calling any other request, view or
exception middleware, or the appropriate view; it’ll apply response
middleware to that HttpResponse, and return the result.
So an HttpResponse will break the middleware stack functionalities. It's more or less the same for the process_view, which will avoid the calls of the exception middlewares. At this point it seems more smart to adopt the second option...
The django-cms plugin confirm this intuition, as you could see from the source code of the urlpatterns definition:
from django.conf import settings
from django.conf.urls import url
from cms.apphook_pool import apphook_pool
from cms.appresolver import get_app_patterns
from cms.views import details
# This is a constant, really, but must live here due to import order
SLUG_REGEXP = '[0-9A-Za-z-_.//]+'
if settings.APPEND_SLASH:
regexp = r'^(?P<slug>%s)/$' % SLUG_REGEXP
else:
regexp = r'^(?P<slug>%s)$' % SLUG_REGEXP
if apphook_pool.get_apphooks():
# If there are some application urls, use special resolver,
# so we will have standard reverse support.
urlpatterns = get_app_patterns()
else:
urlpatterns = []
urlpatterns.extend([
url(regexp, details, name='pages-details-by-slug'),
url(r'^$', details, {'slug': ''}, name='pages-root'),
])