django-rest-framework: add additional permission in ViewSet update method - python

I have the following code:
class UsersViewSet(viewsets.ModelViewSet):
model = Users
permission_classes = (IsAuthenticated,)
def update(self, request, *args, **kwargs):
return super(UsersViewSet, self).update(request, *args, **kwargs)
The question is:
how can I add additional Permission only for update method? (need to get isAuthenticated + Permission)
overwrite permissions only for update method? (need to get only Permission without isAuthenticated)
other methods in viewset should have IsAuthenticated permission
Can I make it with decorator?Or anything else?
Wanna get something like that:
#permission_classes((IsAuthenticated, AdditionalPermission ))
def update:
pass
But if i write this code the second permission is not checked through request

LATER EDIT
As it seems that DRF decorators don't really work (at least not for me), this is the best solution I could come up with:
def get_permissions(self):
# Your logic should be all here
if self.request.method == 'GET':
self.permission_classes = [DummyPermission, ]
else:
self.permission_classes = [IsAuthenticated, ]
return super(UsersViewSet, self).get_permissions()
This actually works for both cases that you asked, but requires a bit more work. However, I've tested it and it does the job.
ORIGINAL ANSWER BELOW
There is a small mistake in the docs, you should be sending a list to the decorator (not a tuple). So it should be like this:
#permission_classes([IsAuthenticated, AdditionalPermission, ])
def update:
pass
To answer your questions:
how can I add additional Permission only for update method?
First of all, you should know that DRF first checks for global permissions (those from the settings file), then for view permissions (declared in permission_classes -- if these exist, they will override global permissions) and only after that for method permissions (declared with the decorator #permission_classes). So another way to do the above is like this:
#permission_classes([AdditionalPermission, ])
def update:
pass
Since ISAuthenticated is already set on the entire view, it will always be checked BEFORE any other permission.
overwrite permissions only for update method?
Well, this is hard(er), but not impossible. You can:
set the permissions for each method and remove it from the class
modify your AdditionalPermission class so that it also checks for user authentication if the method is not update.
Good luck.

You can also specify permissions for specific methods in the get_permissions() method:
class MyViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ('update', 'other_viewset_method'):
self.permission_classes = [permissions.CustomPermissions,]
return super(self.__class__, self).get_permissions()

#permission_classes didn't work for class based view. And I tried #detail_route(permission_classes=(permissions.CustomPermissions,)) for update view function, still not work.
so, my solution is:
class MyViewSet(viewsets.ModelViewSet):
def update(self, request, *args, **kwargs):
self.methods=('put',)
self.permission_classes = (permissions.CustomPermissions,)
return super(self.__class__, self).update(request, *args, **kwargs)
Have a try. my drf is v3.1.1

For me, get_permissions worked but it did turn out that if you sending in Authorization in your header in your request rest framework will throw an error even if permission is set to AllowAny. If you are going to use both authorization and AllowAny you need to have to take this into consideration.

Yes you can by adding annotation
See this link for more information there are examples:
https://docs.djangoproject.com/en/1.6/topics/auth/default/#django.contrib.auth.decorators.permission_required

Related

Indirect way of usine Model.objects.all() in a formset

I'm using something like this to populate inlineformsets for an update view:
formset = inline_formsetfactory(Client, Groupe_esc, form=GroupEscForm, formset=BaseGroupEscInlineFormset, extra=len(Groupe.objects.all()))
(Basically I need as many extra form as there are entries in that table, for some special processing I'm doing in class BaseGroupEscInlineFormset(BaseInlineFormset)).
That all works fine, BUT if I pull my code & try to makemigrations in order to establish a brand new DB, that line apparently fails some django checks and throws up a "no such table (Groupe)" error and I cannot makemigrations. Commenting that line solves the issues (then I can uncomment it after making migration). But that's exactly best programming practices.
So I would need a way to achieve the same result (determine the extra number of forms based on the content of Groupe table)... but without triggering that django check that fails. I'm unsure if the answer is django-ic or pythonic.
E.g. perhaps I could so some python hack that allows me to specific the classname without actually importing Groupe, so I can do my_hacky_groupe_import.Objects.all(), and maybe that wouldn't trigger the error?
EDIT:
In forms.py:
from .models import Client, Groupe
class BaseGroupEscInlineFormset(BaseInlineFormSet):
def get_form_kwargs(self, index):
""" this BaseInlineFormset method returns kwargs provided to the form.
in this case the kwargs are provided to the GroupEsForm constructor
"""
kwargs = super().get_form_kwargs(index)
try:
group_details = kwargs['group_details'][index]
except Exception as ex: # likely this is a POST, but the data is already in the form
group_details = []
return {'group_details':group_details}
GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
extra=len(Groupe.objects.all()),
can_delete=False)
The issue as already outlined is that your code is written at the module level and it executes a query when the migrations are not yet done, giving you an error.
One solution as I already pointed in the comment would be to write the line to create the formset class in a view, example:
def some_view(request):
GroupeEscFormset = inlineformset_factory(
Client,
Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
extra=len(Groupe.objects.all()),
can_delete=False
)
Or if you want some optimization and want to keep this line at the module level to not keep recreating this formset class, you can override the __init__ method and accept extra as an argument (basically your indirect way to call Model.objects.all()):
class BaseGroupEscInlineFormset(BaseInlineFormSet):
def __init__(self, *args, extra=3, **kwargs):
self.extra = extra
super().__init__(*args, **kwargs)
...
GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
can_delete=False)
# In your views:
def some_view(request):
formset = GroupeEscFormset(..., extra=Groupe.objects.count()) # count is better if the queryset is needed only to get a count

How to re-use GET call of another class-based api view

I have a hard time trying to re-use a get call from an existing APIView in another APIVIew.
I have a class-based DRF view:
# in urls.py
path('api/something', views.SomethingList.as_view()),
path('api/similarsomething', views.SomethingList.as_view()), #legacy url
# in views.py
class SomethingList(generics.ListCreateAPIView):
queryset = Something.objects.all()
serializer_class = SomethingSerializer
# override get, because of some required custom action
def get(self, request, *args, **kwargs):
# do some custom actions (scan folder on filesystem)
...
return super().get(request, *args, **kwargs)
The above view both provides a get (list) and post (create) API interface. As intended. I've augmented it with DRF-spectacular information (not shown here) to generate my swagger docs.
Now, I have another (legacy) URL defined that should do exactly the same as the get (list) call above. Currently, this legacy url also points to the SomethingList.
But ... the legacy URL should NOT provide the post (create) interface, and I want to mark it as 'deprecated' in swagger using drf-spectacular. So I figured I need a separate class to restrict to get() and add the #extend_schema decorator
So I though of re-using the existing SomethingList.get functionality as follows:
# in urls.py
path('api/something', views.SomethingList.as_view()),
path('api/similarsomething', views.SimilarSomethingList.as_view()), # ! points to new class
# in views.py
class SomethingList(generics.ListCreateAPIView):
...
class SimilarSomethingList(generics.ListAPIView): #ListAPIView only!
#extend_schema(summary="Deprecated and other info..")
def get(self, request, *args, **kwargs):
view = SomethingList.as_view()
return view.get(request, *args, **kwargs)
However, this doesn't work. I get AttributeError: 'function' object has no attribute 'get'
I tried a couple of variations, but couldn't get that working either.
Question:
How can I reuse the get() call from another APIView? Should be simple, so I'm likely overlooking something obvious.
Set http_method_names to the class view.
class SomethingList(generics.ListCreateAPIView):
http_method_names = ['get', 'head']
reference: https://stackoverflow.com/a/31451101/13022138

How do I pass the request object to the method_decorator in django class views?

I have been working on this all day.
I am trying to write custom permission for class views to check if user is in a certain group of permissions.
def rights_needed(reguest):
if request.user.groups.filter(Q(name='Admin')).exists():
pass
else:
return HttpResponseRedirect('/account/log-in/')
#method_decorator(rights_needed, name='dispatch')
class AdminView(CreateView):
model = Admin
form_class = AdminForm
def get_template_names(self):
return 'clinic/visitform_list.html'
Could help me know how I can achieve this? Or an easier way around it?
I also tried this (code inside AdminView class):
def dispatch(self, request):
if request.user.groups.filter(Q(name='Admin')).exists():
return super().dispatch(*args, **kwargs)
else:
return HttpResponseRedirect('/account/log-in/')
A decorator is a function that takes a function (a view in this case), and returns another function (a view in this case). At the moment your rights_needed looks like a regular view - it’s returning a response not a function.
Django comes with a user_passes_test method that makes it easy to create decorators like this. Since you are using class based views, it would be even easier to use the UserPassesTest mixin.
Your test function for the mixin would be:
def test_func(self):
return self.request.user.groups.filter(Q(name='Admin')).exists()

Permission class on #detail_route not working, Django Rest Framework

I'm new to DRF, but I'm trying to use a permission class on a #detail_route using the method in this stack thread: Using a permission class on a detail route
My code currently looks like this :
#detail_route(methods=['GET'], permission_classes=[IsStaffOrRestaurantUser])
def restaurant_dishes_ready_for_pickup(self, request, pk=None):
...stuff....
class IsStaffOrRestaurantUser(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print(request)
print(view)
print(obj)
return False
The print statements never get executed... I'm probably missing something but I've looked through the documentation and can't really figure it out, is my approach right at all? Thanks!
EDIT:
I realize in our code already that we have this snippet in our Viewset, is it possible to override this in the decorator?
def get_permissions(self):
# Limit to listing and getting for non Admin user
if self.request.method in permissions.SAFE_METHODS:
return (permissions.AllowAny(),)
return (IsAdminUser(),)
Not sure if it's the most elegant solution, but you might be able to upgrade get_permissions() like so:
def get_permissions(self):
# check additional route specifics
path = self.request.path
if ("restaurant_dishes_ready_for_pickup" in path):
return (IsStaffOrRestaurantUser,)
# Limit to listing and getting for non Admin user
if (self.request.method in permissions.SAFE_METHODS):
return (permissions.AllowAny,)
return (IsAdminUser,)
PS: Also maybe return permission class objects instead of instances in get_permissions().
Quote from the documentation:
If you're writing your own views and want to enforce object level permissions, or if you override the get_object method on a generic view, then you'll need to explicitly call the .check_object_permissions(request, obj) method on the view at the point at which you've retrieved the object.
So you'll need to call explicitly the permission check.
Note that you could have that for free if you were using a RetrieveAPIView instead of a function based view for example.

Django Rest Update

I am trying to update data via Django Rest PUT method.
class TableView(generics.ListAPIView, generics.UpdateAPIView, generics.CreateAPIView):
serializer_class = TableSerializer
def update(self, request, *args, **kwargs):
if kwargs.__len__() != 0:
tableid = kwargs['id']
mycol = request.DATA['col']
Table.objects.filter(id=tableid).update(col=mycol)
So, this works but data is updated by Table.object which is model. Is there any generic way to update data? I mean, if I PUT col1 and col2 data, it will update them.. If I send only col1, it will update just it..
yes you can use something like this
def update(self, instance,validated_data):
# The instance is the already saved object
instance.col=validated_data['col']
instance.col2=validated_data['col2']
instance.save()
return instance
check this link for the .update() method http://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
This is what PATCH requests does for you. However, in some cases PATCH is not supported (some older versions of Lighttpd for instance) or you might need to use PUT for legacy reasons.
The good thing is that PATCH and PUT requests are almost the same in Django REST Framework. They share most of the code, and the main difference is that PATCH requests sets a parameter partial to True.
What you could try to do is, in your update() method by manually settings the partial flag and passing it along to your serializer like:
def update(self, request, *args, **kwargs):
partial = True
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
This should only update the parts that was passed as arguments in the request.
Normally a PATCH request will call the perform_update() function in your ModelViewSet, and just set the partial flag to true and then call the update() function after that. Source: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py#L76
Some info from the Django REST Framework page about the update mixin: http://www.django-rest-framework.org/api-guide/generic-views/#updatemodelmixin

Categories