I create a custom action method in Django ViewSet and I see detail argument. If I set detail=True I can't call this method from URL but if I set detail=False, I can call this method. May I know what is the meaning of detail argument?
Here is my viewset = >
class TimeSheetViewSet(viewsets.ModelViewSet):
queryset = TimeSheet.objects.all()
serializer_class = TimeSheetSerializer
#action(methods=['get'], detail=True)
def byhello(self, request):
return Response({"From Hello":"Got it"})
Here are router and URL patterns =>
router.register('timesheets_ts', TimeSheetViewSet, base_name='timesheets')
urlpatterns = [
path('api/', include(router.urls))
]
As the docs states, if you pass detail=True it means that that router will return you a single object whilst if you don't pass detail=True or pass detail=False it will return a list of objects.
One thing to have in mind is that if you are not doing anything or don’t need a single object in this function, you can set the detail=False
In your case it’d be something like:
#action(methods=['get'], detail=True)
def byhello(self, request, pk=None):
self.object = self.get_object()
return Response({"From Hello":"Got it"})
Related
I have a little news app with a DetailView class like the following:
class DetailView(LoginRequiredMixin,generic.DetailView):
model = NewsItem
template_name = 'news/detail.html'
def get_object(self):
obj = super().get_object()
if self.request.user.is_superuser or obj.published:
return obj
and an urls.py config like this:
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
path('<int:itemId>/publish', views.publish, name='publish'),
]
Now when i pass an invalid ID to the detail view it automatically redirects to 404. I wanted to know if it's possible to just pass an empty object to the DetailView instead. The template then would handle the 404 on its own.
Also:Even though it works so far I feel like the way i handled the permission requirements (overriding the get_object method) isn't the correct way/Django way to do things
Solution: i changed the overriding of get_object like this:
class DetailView(LoginRequiredMixin,generic.DetailView):
model = NewsItem
template_name = 'news/detail.html'
def get_object(self):
queryset = self.get_queryset()
pk = self.kwargs.get(self.pk_url_kwarg)
if pk is not None:
queryset = queryset.filter(pk=pk)
else:
raise AttributeError("No article id provided")
try:
return queryset.get()
except queryset.model.DoesNotExist:
return None
it is basically the same get_object method as the original just without the check for a slug value, and if the queryset.get() call catches the queryset.model.DoesNotExist Exception it returns None
Classy CBVs, as usual, makes all clear.
If you don't like 404 for a bad id, you need to override get_object to a greater extent than you have done, because it raises the exception Http404 for a bad id. The code in question is
try:
# Get the single item from the filtered queryset
obj = queryset.get()
except queryset.model.DoesNotExist:
raise Http404(_("No %(verbose_name)s found matching the query") %
{'verbose_name': queryset.model._meta.verbose_name})
return obj
You could catch the exception when you call obj = super().get_object()
try:
obj = super().get_object()
except Http404:
obj = NewsItem( ... ) # a dummy empty NewsItem
Or you could simply not call super() at all, and supply your own code to return an appropriate object. The empty object might exist in the database as a special entity (best created by a data migration), or it might (as here) be created on the fly and never saved. In either case it would have some distinguishing characteristic that the template can easily test.
I am new in Django and I am bit confused in Django apiview for custom method.
In ApiView, How can I create a custom method and How to call from axios.
For example
Here is my View
class TimeSheetAPIView(APIView):
#action(methods=['get'], detail=False)
def getbytsdate(self, request):
return Response({"timesheet":"hello from getbydate"})
def get(self,request,pk=None):
if pk:
timesheet=get_object_or_404(TimeSheet.objects.all(),pk=pk)
serializer = TimeSheetSerializer(timesheet)
return Response({serializer.data})
timesheet=TimeSheet.objects.all()
serializer = TimeSheetSerializer(timesheet,many=True)
return Response({"timesheet":serializer.data})
Here is my URL=>
url(r'^timesheets_ts/(?P<pk>\d+)/$', TimeSheetAPIView.as_view()),
url(r'^timesheets_ts/', TimeSheetAPIView.as_view()),
Normally my url would be like=>
api/timesheet_ts/
this one will get all of my records.
So my question is how can I setup URL for getbytsdate or getbyname or other some kind of custom get method? and how can I call?
I tried like this way=>
url(r'^timesheets_ts/getbytsdate/(?P<tsdate>[-\w]+)/$', TimeSheetAPIView.as_view()),
and I called like that
api/timesheets_ts/getbytsdate/?tsdate='test'
Its not work.
So please can u explain for the custom method in apiview and url setting?
In addition to your implementation, you just need to show your custom get request to your urls.py. Edit your urls.py as follows:
# urls.py
timesheet_getbytsdate_detail = TimeSheetAPIView.as_view({'get': 'getbytsdate'})
timesheet_detail = TimeSheetAPIView.as_view({'get': 'retrieve'})
urlpatterns = [
url(r'^timesheets_ts/getbytsdate/(?P<tsdate>[-\w]+)/$', getbytsdate_detail),
url(r'^timesheets_ts/(?P<pk>[0-9]+)/', timesheet_detail),
]
EDIT: You need to use the combination viewsets.GenericViewSet and mixins.RetrieveModelMixin instead of APIVewto get use of that:
class TimeSheetAPIView(viewsets.GenericViewSet, mixins.RetrieveModelMixin):
#action(methods=['get'], detail=False)
def getbytsdate(self, request):
return Response({"timesheet":"hello from getbydate"})
def retrieve(self, request, *args, **kwargs):
timesheet=self.get_object()
serializer = TimeSheetSerializer(timesheet)
return Response({serializer.data})
timesheet=TimeSheet.objects.all()
serializer = TimeSheetSerializer(timesheet,many=True)
return Response({"timesheet":serializer.data})
I've created 2 ModelViewSets like so (simplified for demonstration):
class SomeBaseViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SomeEventSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self):
return SomeObjects.objects.filter(owner=self.request.user)
def get_serializer_context(self):
context = super(SomeBaseViewSet, self).get_serializer_context()
context.update({
"user": self.request.user,
"some_context_key": False
})
return context
class AdminViewSet(SomeBaseViewSet):
# Added in the HasAdminPermission
permission_classes = (permissions.IsAuthenticated, HasAdminPermission)
# Different queryset filter (overriding `get_queryset` vs setting queryset for standardization)
def get_queryset(self):
return SomeObjects.objects.all()
# The context should have `some_context_key=True`, and `user=request.user`
def get_serializer_context(self):
context = super(AdminViewSet, self).get_serializer_context()
context.update({
"some_context_key": True
})
return context
My router/url config looks like this
router = DefaultRouter()
router.register(r'some_view', SomeBaseViewSet, base_name="some_view")
urlpatterns += [
url(r'^api/', include(router.urls)),
]
If I wanted to route /api/some_view/admin to the AdminViewSet, what's the best way to do that?
Things I've tried:
#list_route on SomeBaseViewSet, but couldn't figure out the "right" way to wire it to my AdminViewSet
Adding url(r'^api/some_view/admin$', AdminViewSet.as_view({"get": "list"})) to my urlpatterns (which sorta works but neuters the ViewSet a bit, and is generally really manual anyway):
Having a dedicated DefaultRouter for the some_view viewset, which I then mount on url(r'^api/some_view/') - again hacky and pedantic to do with a large number of routes
Is there a "right" way to do what I'm trying to accomplish, or should I reach for a different solution to this problem (i.e. a filter or something)?
I've seen libraries like https://github.com/alanjds/drf-nested-routers, but that seems like overkill for my (fairly simple) needs.
Not sure if i've missed something but i've just tested and this works perfectly (the order is important):
router = DefaultRouter()
# this will overrides routes from the line below
router.register(r'some_view/admin', AdminViewSet)
router.register(r'some_view', SomeBaseViewSet)
Define your admin viewset using a list route. These params will allow you to perform a get request, with specified permissions(is authenticated and has admin perms), that extends this class. ie /someview/admin or someotherview/admin
from rest_framework.decorators import list_route
class AdminViewSet(viewset.Viewsets):
#list_route(methods=['get'],
permissions=[permissions.IsAuthenticated, HasAdminPermission],
url_path='admin'
)
def admin(self, request):
# All your custom logic in regards to querysets and serializing
return serialized.data
Then you can extend your any viewset that needs an admin action route.
class SomeBaseViewSet(viewsets.ReadOnlyModelViewSet, AdminViewset):
serializer_class = SomeEventSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self):
return SomeObjects.objects.filter(owner=self.request.user)
def get_serializer_context(self):
context = super(SomeBaseViewSet, self).get_serializer_context()
context.update({
"user": self.request.user,
"some_context_key": False
})
return context
You want to be careful with this because typically the param after your base route ie /someview/{param}/ is reserved for ID references. Make sure your id reference will not conflict with your detail route.
I think I've found an answer to my own question, but I put a +50 rep bounty on this in case someone wants to chime in (#tom-christie maybe?).
Either way the way that I've solved it for my usecase is by using the #list_route and the AdminViewSet's .as_view() function.
Something like this suffices:
class SomeBaseViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = SomeEventSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self):
return SomeObjects.objects.filter(owner=self.request.user)
def get_serializer_context(self):
context = super(SomeBaseViewSet, self).get_serializer_context()
context.update({
"user": self.request.user,
"some_context_key": False
})
return context
#list_route()
def admin(self, request):
return AdminViewSet.as_view({"get": "list"})(request)
class AdminViewSet(SomeBaseViewSet):
# Added in the HasAdminPermission
permission_classes = (permissions.IsAuthenticated, HasAdminPermission)
# Different queryset filter (overriding `get_queryset` vs setting queryset for standardization)
def get_queryset(self):
return SomeObjects.objects.all()
# The context should have `some_context_key=True`, and `user=request.user`
def get_serializer_context(self):
context = super(AdminViewSet, self).get_serializer_context()
context.update({
"some_context_key": True
})
return context
And will allow one to have the url's routed accordingly (based on the name of the function), and enforce any extra things you need.
good question. i'd check out DRF's detail_route for this -- this is an idiom i've used successfully in the past to create a one-off type of endpoint that hangs off of a viewset. HTH,
http://www.django-rest-framework.org/api-guide/routers/#extra-link-and-actions
I'm trying to display an absolute URI in the list display of a Django admin:
from django.contrib import admin
class MyAdmin(admin.ModelAdmin):
list_display = (
'full_uri',
)
readonly_fields = (
'full_uri',
)
def full_uri(self, obj):
return u'<a href="{url}">{url}'.format(
url = request.build_absolute_uri(reverse('view_name', args=(obj.pk, )))
)
full_uri.allow_tags = True
The problem - I don't have access to the request object (in which build_absolute_uri is defined).
I tried to override changelist_view and store request localy, but this is not thread safe.
I also tried this solution, but it's not very elegant.
Any other ideas?
EDIT:
Is this a safe solution:
def changelist_view(self, request, extra_context=None):
self.request = request
try:
return super(MyAdmin,self).changelist_view(request, extra_context=extra_context)
finally:
self.request = None
EDIT2:
Not thread-safe at all! 1 2, ...
Every class that extends admin.ModelAdmin comes with two functions you can override: change_view, and changelist_view. change_view is called when you edit a single instance, changelist_view is called when viewing the list of all items.
Those two functionas act as the view definition for the admin and contain the request object for the page. This means that within those two function, you could store the request as a property of self to make it accessible to other functions within the class called from that view. With that, being said, something like this should work:
from django.contrib import admin
class MyAdmin(admin.ModelAdmin):
list_display = (
'full_uri',
)
readonly_fields = (
'full_uri',
)
def change_view(self, request, object_id, form_url='', extra_context=None):
# access the request object when in the change view
self.request = request
return super(MyAdmin, self).change_view(request, object_id, form_url=form_url, extra_context=extra_context)
def changelist_view(self, request, extra_context=None):
# access the request object when in the list view
self.request = request
return super(MyAdmin,self).changelist_view(request, extra_context=extra_context)
def full_uri(self, obj):
return u'<a href="{url}">{url}'.format(
# access the request object through self.request, as set by the two view functions
url = self.request.build_absolute_uri(reverse('view_name', args=(obj.pk, )))
)
full_uri.allow_tags = True
I think I found a way to do it in a thread-safe manner, create a function inside get_list_display:
def get_list_display(self, request):
def link_to_view(obj):
return u'<a href="{url}">{url}'.format(
url = request.build_absolute_uri(reverse('view_name', args=(obj, )))
)
token_access_link.short_description = _('link')
token_access_link.allow_tags = True
return (
link_to_view,
)
If I've understood the question correctly, you just need access to the request object in a function that hasn't been sent it?
If so, I've found django-globals to be the most helpful as it provides universal access to the current request and user (which should be accessible by default like in flask!)
pip install django-globals
settings.py
INSTALLED_APPS = [
...
'django_globals'
]
MIDDLEWARE_CLASSES = [
...
'django_globals.middleware.Global'
]
view.py
from django_globals import globals as djglobals
def myfunc():
djglobals.user
djglobals.request
I have a blog app that uses django_taggit. My HomePageView subclasses ArchiveIndexView and works well.
Now I'd like the following link to work: http://mysite.com/tag/yellow and I'd like to use the ArchiveIndexView generic class and pass in a modified queryset that filters on tag_slug. I want to do this because I want to use the same template as the homepage.
My urls.py is
url(r'^$', HomePageView.as_view(paginate_by=5, date_field='pub_date',template_name='homepage.html'),
),
url(r'^tag/(?P<tag_slug>[-\w]+)/$', 'tag_view'), # I know this is wrong
My views.py is
class HomePageView(ArchiveIndexView):
"""Extends the detail view to add Events to the context"""
model = Entry
def get_context_data(self, **kwargs):
context = super(HomePageView, self).get_context_data(**kwargs)
context['events'] = Event.objects.filter(end_time__gte=datetime.datetime.now()
).order_by('start_time')[:5]
context['comments'] = Comment.objects.filter(allow=True).order_by('created').reverse()[:4]
return context
I realize I'm lost here, and would like some help in finding out how to create a new class TagViewPage() that modifies the queryset by filtering on tag_slug.
The key thing is to override the get_queryset method, so that the queryset only includes returns entries with the chosen tag. I have made TagListView inherit from HomePageView, so that it includes the same context data - if that's not important, you could subclass ArchiveIndexView instead.
class TagListView(HomePageView):
"""
Archive view for a given tag
"""
# It probably makes more sense to set date_field here than in the url config
# Ideally, set it in the parent HomePageView class instead of here.
date_field = 'pub_date'
def get_queryset(self):
"""
Only include entries tagged with the selected tag
"""
return Entry.objects.filter(tags__name=self.kwargs['tag_slug'])
def get_context_data(self, **kwargs):
"""
Include the tag in the context
"""
context_data = super(TagListView, self).get_context_data(self, **kwargs)
context_data['tag'] = get_object_or_404(Tag, slug=self.kwargs['tag_slug'])
return context_data
# urls.py
url(r'^tag/(?P<tag_slug>[-\w]+)/$', TagListView.as_view(paginate_by=5, template_name='homepage.html')),