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.
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 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.
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.
Is it any possibility to get all actions names from ViewSet in DRF?
I mean not only standard list, retrieve etc but custom ones too (defined by #action decorator)
I've tried to use action_map but it is empty
I don't think there is a direct way to get all the actions specified in a ViewSet class. But, the Routers are usually generating the URLs in a similar way. So, I'm going to use the DRF Routers here,
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
routes = router.get_routes(YourViewsetClass)
action_list = []
for route in routes:
action_list += list(route.mapping.values())
distinct_action_list = set(action_list)
I'm using rest_framework version 3.11.2 and there you can do it like that:
actions = [action for action in YourViewsetClass.get_extra_actions()]
action_names = [action.__name__ for action in YourViewsetClass.get_extra_actions()]
action_url_names = [action.url_name for action in YourViewsetClass.get_extra_actions()]
I am creating REST based APIs for an app using Tastypie with Django. The problem is default API url in Tastypie contains version info in url patterns i.e.
http://lx:3001/api/v1/vservers/?username=someuser&api_key=someapikey
I want my url to be free from API version info like this:
http://lx:3001/api/vservers/?username=someuser&api_key=someapikey
urls.py
v1_api = Api()
v1_api.api_name = ''
v1_api.register(UserResource())
...
url(r'^api/', include(v1_api.urls)),
I am overwriting api_name with an empty string still
http://lx:3001/api/vservers/?username=someuser&api_key=someapikey does not work.
How can I get rid of the version info altogether?
Thanks..
Subclass Api and override urls to remove all the api_name-related bits:
class MyApi(Api):
#property
def urls(self):
"""
Provides URLconf details for the ``Api`` and all registered
``Resources`` beneath it.
"""
pattern_list = [
url(r"^%s$" % trailing_slash(), self.wrap_view('top_level'), name="api_top_level"),
]
for name in sorted(self._registry.keys()):
pattern_list.append((r"^/", include(self._registry[name].urls)))
urlpatterns = self.override_urls() + patterns('',
*pattern_list
)
return urlpatterns
Although tastypie makes supplying api_name optional, failing to provide one simply defaults api_name to "v1". This behavior can be modified by subclassing Api and overriding the urls property within api.py to behave independently of api_name. To achieve the desired URLconf, however, there's still one correction to #dokkaebi's solution worth noting:
pattern_list.append((r"^/", include(self._registry[name].urls)))
should instead read:
pattern_list.append((r'', include(self._registry[name].urls)))
in order to avoid the dreaded // that would direct your clients to
http://lx:3001/api//vservers/?username=someuser&api_key=someapikey
in place of
http://lx:3001/api/vservers/?username=someuser&api_key=someapikey
as intended.
For convenience, I've included the modified code below.
Solution
class MyApi(Api):
"""
An API subclass that circumvents api_name versioning.
"""
#property
def urls(self):
"""
Provides URLconf details for the ``Api`` and all registered
``Resources`` beneath it.
"""
pattern_list = [
url(r"^%s$" % trailing_slash(), self.wrap_view('top_level'), name="api_top_level"),
]
for name in sorted(self._registry.keys()):
pattern_list.append((r'', include(self._registry[name].urls)))
urlpatterns = self.override_urls() + patterns('',
*pattern_list
)
return urlpatterns
Convention
One of Django’s core philosophies is that URLs should be beautiful; a clean, elegant URL scheme is an important detail in any high-quality Web application. With respect to the validity of this approach, using a custom request header or an accept header will get the versioning job done without convoluting the scheme with the (subjectively) ugly v1/. That is not to say that the URL versioning strategy is without its share of caveats; however, it's quick to implement and predictable in its response.