<int:pk> or {pk} does not work in DRF router - django

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)),
]

Related

Django rest_framework DefaultRouter() class vs normal url

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.

How to use parameters in DRF router

Hello guys so I came to realise that DRF ModelViewSet is a quick way to make rest views, I am trying to use parameters within the router url but the ViewSet does not detect the param
Here is how my viewset looks
class ClientRequests(ModelsViewSet):
serializer_class = serializers.ClientRequestSerializer
def get_queryset(self):
return models.ClientRequest.objects.filter(cliend_id=self.request.kwargs.get('client_id')
now I register the router as below
router = DefaultRouter()
router.register('/<int:client_id/requests', views.ClientRequests, basename='clients request api endpoint')
urlpatterns=[
path('', include(router.urls))
Is this the right way to use restframework router and how can I pass paramters to the url when using router
Use regex notation
router.register(
r'(?P<client_id>\d+)/requests',
views.ClientRequests,
basename='clients request api endpoint'
)

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.

django-rest defaultRouter doesn't add #action

I am reading http://www.django-rest-framework.org/api-guide/routers#usage and can't understand what a base_name is. Also i try to add a custom action and the router won't pick it up
I have this views.py
#authentication_classes((SessionAuthentication, TokenAuthentication))
#permission_classes((IsAuthenticated,))
class utente(CreateModelMixin, RetrieveAPIView, GenericViewSet, ViewSet):
model = MyUser
serializer_class = MyUserSerializer
def retrieve(self, request, *args, **kwargs):
self.object = MyUser.objects.get(
pk = request.user.pk
)
serializer = MyUserSerializerGET(self.object)
return Response(serializer.data)
#action(permission_classes=[IsAuthenticated])#POST action
def customaction(self, request):
return Response( None )
pass
and this urls.py
admin.autodiscover()
router_v1 = routers.DefaultRouter(trailing_slash=True)
router_v1.register(r'register', my_register, 'wtf' )
router_v1.register(r'utente', utente, 'wtf2' )
#router_v1.register(r'utente/customaction', utente.as_view({'post' : 'customaction'}) )
api_urls_v1 = router_v1.urls
api_urls = patterns('',
url(r'^v1/', include(api_urls_v1)),
)
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'wecup.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
url(r'^admin/', include(admin.site.urls)),
url(r'^login/', 'rest_framework.authtoken.views.obtain_auth_token'),
url(r'^logout/', my_logout ),
url(r'^api/', include(api_urls)),
)
when i open http://127.0.0.1:8000/api/v1/
HTTP 200 OK Content-Type: application/json Vary: Accept Allow: GET, HEAD, OPTIONS
{
"register": "http://127.0.0.1:8000/api/v1/register/",
"utente": "http://127.0.0.1:8000/api/v1/utente/"
where is customaction?
}
You've got two different questions here, so I'll address them each separately.
Base Name
First, a base_name is simply the name the ViewSet will use when generating your named urls. By default, this will simply be your model or perhaps your queryset, though you may need to set it automatically if you've played with your ViewSet's get_queryset method.
If you don't implement your own url names, then the base_name will be used to implement them for you. For example, given that your Model is MyUser, your named urls would be something like 'myuser-list' or 'myuser-detail'.
Documentation, if interested, is here.
#action and custom methods
You're using a DefaultRouter, which allows you to access the API root view at http://127.0.0.1:8000/api/v1/, as you've shown. This root view only shows list views. Using #action creates a detail view. In your case, your customaction view can be found at ^utente/{pk}/customaction/$. It will not show up in the API root because it is not a list view.
General information on #action and custom methods can be found here.
Also, if for some reason you do want to make customaction a list-level view, you'll need to make some modifications. You could either string up a custom route yourself, without using the #action decorator (which is specifically for detail views). An example of that can be found here.
Your other option is to use the new-ish drf-extensions package. A discussion of using the package to implement collection level controllers in ViewSets can be found here.

How to use url pattern named group with generic view?

I'm trying to display blog records for particular author using generic view:
urlpatterns = patterns('',
url(r'^blog/(?P<uid>[\d+])/$', ListView.as_view(
queryset=Blog.objects.filter(published=True, author=uid),
), name='blog_list'),
But I get NameError: name 'uid' is not defined
Is it possible to use urlconf named groups this way?
You need to create your own implementation of ListView like so:
class BlogListView(ListView):
model = Blog
def get_queryset(self):
return super(BlogListView, self).get_queryset().filter(
published=True, author__id=self.kwargs['uid'])
and then use it in your URLconf:
urlpatterns = patterns('',
url(r'^blog/(?P<uid>[\d+])/$', BlogListView.as_view(),
name='blog_list'),
The documentation for class-based generic views is, in my opinion, not quite up to scratch with the rest of the Django project yet - but there are some examples which show how to use ListView in this way:
https://docs.djangoproject.com/en/1.3/topics/class-based-views/#viewing-subsets-of-objects