Django Rest Framework permissions not being called - python

I'm currently converting all my views to generics, as I like how cleaner the code gets. I am trying to make my User detail view, like so:
# User.views
from Common import view_mixins, view_filters, view_permissions
class UserDetail(view_mixins.IntOrStrLookupMixin, generics.RetrieveUpdateDestroyAPIView):
queryset = Profile.objects.all()
lookup_fields = ('user__pk', 'user__username')
lookup_url_kwarg = 'userid'
filter_backends = (view_filters.ResourceVisibilityFilter, )
permission_classes = (view_permissions.IsOwnerOrReadOnly, )
serializer_class = ProfileSerializer
def update(self, request, *args, **kwargs):
user = self.get_object()
return Response('whatever')
# Common.view_permissions
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
class IsOwnerOrReadOnly(permissions.BasePermission):
'''
Owner of object can GET, PUT, DELETE. Everyone else can GET.
'''
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
print('did you call me?')
return (
request.method in SAFE_METHODS
or
obj.user == request.user
)
# Common.view_mixins
class IntOrStrLookupMixin(object):
"""
Apply to views that can be looked up by slug or pk
"""
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.kwargs:
argument = self.kwargs[field]
if is_int(argument):
filter[self.lookup_fields[0]] = argument
else:
filter[self.lookup_fields[1]] = argument
return get_object_or_404(queryset, **filter)
So my issue is, the permission is never called. I can get the user just fine, but anyone can do PUT or DELETE on which I am trying to prevent.

In my mixin, I had to call the check_object_permissions method.
class IntOrStrLookupMixin(object):
"""
Apply to views that can be looked up by slug or pk
"""
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.kwargs:
argument = self.kwargs[field]
if is_int(argument):
filter[self.lookup_fields[0]] = argument
else:
filter[self.lookup_fields[1]] = argument
obj = get_object_or_404(queryset, **filter)
self.check_object_permissions(self.request, obj)
return obj

I think I encountered this problem. The object level permissions are only called when you use get_object() method to get the object being operated on. Update your update() with this line and you should see the permissions being called. And update your custom get_object() to either call the super method or call the permissions directly
class UserDetail(view_mixins.IntOrStrLookupMixin, generics.RetrieveUpdateDestroyAPIView):
... blah blah blah
def update(self, request):
user = self.get_object()
return Response('whatever')
class IntOrStrLookupMixin(object):
def get_object(self):
... retrieve the object ...
self.check_object_permissions(self.request, obj)
return obj
Edit: I filed a bug report about this with the DRF team and they updated the docs. On the permission page in the docs it says "Object level permissions are run by REST framework's generic views when .get_object() is called". I agree that this is a rather subtle thing and is easy to miss.
Edit #2: Looks like the problem is not only in the update() not calling get_object(), but also in the IntOrStrLookupMixin mixin redefining get_object() method. Updated the code to reflect

Related

How to test django (3.0) CreateView's 'form_valid' method and parameters with a test client?

Some info
I'm overriding the form_valid method (for one reason or another).
What I'm trying to do
I want to test the form_valid instance, and specifically its arguments. To do so, I'm using Django's test client.
Some code
models.py:
class MyModel(models.Model):
my_model_text = models.CharField(max_length=100)
my_model_date = models.DateTimeField(
'my model date',
auto_now_add=True)
views.py:
class CreateMyModelView(LoginRequiredMixin, generic.edit.CreateView):
model = MyModel
template_name = 'myapp/create-my-model.html'
form_class = CreateMyModelForm
def post(self, request=None, *args, **kwargs):
# do something here
form = self.get_form()
if form.is_valid():
return self.form_valid(form, request)
else:
return self.form_invalid(form)
def form_valid(self, form, request):
# do something else here
my_model_text = form.cleaned_data['my_model_text']
MyModel.objects.create(my_model_text=my_model_text)
return redirect(reverse('myapp:mypage'))
forms.py:
class CreateMyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['my_model_text']
tests.py:
class CreateMyModelViewTests(TestCase):
#classmethod
def setUpTestData(cls):
cls.my_auth_user = User.objects.create(
username='my_auth_user')
cls.my_auth_user.set_password('my_auth_password')
cls.my_auth_user.save()
def test_form_valid(self):
client = self.client
client.login(
username='my_auth_user',
password='my_auth_password')
# post response ? #
Question
How can I test the arguments that form_valid gets with the test-client (not an instance of the form object, but the form_valid method itself)?
Since CBVs store the current request as self.request, you don't need to change the signature of form_valid.
I think your view can/should be simplified to something like this (and as you can see, there's just commented-out stuff in form_valid() too, so you can elide it entirely unless you need to modify the instance using e.g. the request user).
class CreateMyModelView(LoginRequiredMixin, CreateView):
model = MyModel
template_name = 'myapp/create-my-model.html'
form_class = CreateMyModelForm
success_url = reverse_lazy('myapp:mypage')
def form_valid(self, form):
# You can modify the object-to-be-saved before saving, e.g.
# form.instance.creator = self.request.user
# The CreateView implementation just calls `form.save()` and redirects away,
# so let's reuse that.
return super().form_valid(form)

Django DRF: #permission_classes not working

I have a view with a custom action which should have a custom permission "IsRightUser". However, the has_object_permission of it is never called, even though I try to access the object with self.get_object() in my view.
class MyView(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = MySerializer
lookup_field = 'uuid'
queryset = MyObject.objects.all()
#action(methods=['get'], detail=True)
#permission_classes([IsRightUser])
def groups(self, request, uuid=None):
# prints [<class 'rest_framework.permissions.IsAuthenticated'>]
print(self.permission_classes)
my_object = self.get_object()
groups = Group.objects.filter(my_object=my_object)
serializer = MySerializer(groups, many=True)
return Response(serializer.data)
Here you can see my custom permission which is never called.
class IsRightUser(BasePermission):
def has_object_permission(self, request, view, obj):
# never called
return True
When I use permission_classes = [IsRightUser] in my view (i.e. directly underneath the lookup_field) it works (unfortunately this is not feasible for me).
Any help is very much appreciated.
You should pass permission classes as action argument directly:
#action(methods=['get'], detail=True, permission_classes=[IsRightUser])
def groups(self, request, uuid=None):
# prints [<class 'rest_framework.permissions.IsAuthenticated'>]
print(self.permission_classes)
my_object = self.get_object()
groups = Group.objects.filter(my_object=my_object)
serializer = MySerializer(groups, many=True)
return Response(serializer.data)
The first decorator perfectly works as soon as you define a default DEFAULT_AUTHENTICATION_CLASSES into settings.py under REST_FRAMEWORK for instance.

Cant create a record within viewset custom view based on url parameters

Hello I have a django rest framework view set. For the create view I want to create a custom view that will create a new record based on two different parameters that are passed on through the url which are namespace and path. I looked at the documentation but i couldnt find how it should look like. I am noit sure what I need to do in order to create a record based on both url parameters.
I basically tried setting the create to a CreateAPIView but it did not work
class PreferenceViewSet(viewsets.ViewSet):
queryset = Preference.objects.all()
serializer_class = PreferenceSerializer
def get_permissions(self):
if self.action == 'create' or self.action == 'destroy':
permission_classes = [IsAuthenticated]
else:
permission_classes = [IsAdminUser]
return [permission() for permission in permission_classes]
def list(self, request):
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
def create(self, request):
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
I want to setup the create to create a preference with the two parameters that are passe in the url
path('preferences/<str:namespace>/<str:path>', preference_path, name='preference-path'),
I wanted it to create a new object with the namespace and path
You need to do this in 2 steps:
Add the url arguments to serializer context from viewset
Override create method on the serializer and use data passed on the context to create the record
So, at first override get_serializer_context method to add the arguments to context:
class PreferenceViewSet(viewsets.ViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._namespace = None
self._path = None
...
...
def get_serializer_context(self):
context = super().get_serializer_context()
context.update(namespace=self._namespace, path=self._path)
return context
def create(self, request):
self._namespace = self.kwargs['namespace']
self._path = self.kwargs['path']
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
Now, you can access the parameters inside the overriden create method of the serializer and create the record as you want e.g.:
class PreferenceSerializer(serializers.HyperlinkedModelSerializer):
...
...
def create(self, validated_data):
namespace = self.context['namespace']
path = self.context['path']
# Create object here based on the params

Django-filters does not work with the Viewset

I have been trying to use django-filters but the objects are not getting filtered. Also, the permission is not working for the partial_update views
I have a Viewset which has the basic actions like - list(), retrieve(), destroy(), partial_update() and few other actions, and trying to apply filter for the same.
After some research I found that since I am creating the queryset via filters I will have to override the get_queryset() method. However, that also doesn't seem to be working. Does the filter works only with ModelViewSet or ListApiView?
ViewSet -
class PostViewSet(viewsets.ViewSet):
"""
The Endpoint to list, retrieve, create and delete Posts.
"""
filter_backends = (DjangoFilterBackend, )
# filterset_class = PostFilter
filter_fields = ('pet_age', 'pet_gender', 'breed')
def get_permissions(self):
if self.action == 'partial_update' or self.action == 'update':
permission_classes = [IsPostAuthor, ]
elif self.action == 'create' or self.action == 'destroy':
permission_classes = [IsAuthenticated, ]
else:
permission_classes = [AllowAny, ]
return[permission() for permission in permission_classes]
def get_queryset(self):
return Post.objects.active() # This is implemented via custom Manager
def list(self, request, *args, **kwargs):
"""
Method for Post listing. It can be accessed by anyone.
"""
serializer = PostListSerializer(self.get_queryset(), many=True, context={"request": request})
return Response(serializer.data)
# REST CODE TRUNCATED
Permission -
class IsPostAuthor(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
if request.user.is_authenticated:
if view.action in ['partial_update', 'update']:
return obj.user.id == request.user.id
return False
return False
PostFilter -
class PostFilter(filters.FilterSet):
class Meta:
model = Post
fields = ('pet_age', 'pet_gender', 'breed', )
Manager -
class PostManager(models.Manager):
def active(self):
return self.filter(post_status='Active')
Any help will be highly appreciated.
Okay, So finally found the solution from DRF Docs. The issue was that in case of normal ViewSet you have to override the method filter_queryset() and return the appropriate queryset accordingly. Then use the queryset under filter_queryset as mentioned by Aman -
serializer = PostListSerializer(self.filter_queryset(self.get_queryset()), many=True, context={"request": request})
Below is the code for reference for those who are still facing issues -
filter_queryset -
def filter_queryset(self, queryset):
filter_backends = (DjangoFilterBackend, )
# Other condition for different filter backend goes here
for backend in list(filter_backends):
queryset = backend().filter_queryset(self.request, queryset, view=self)
return queryset
You have overwrite the list method, so it's not working to work call the filter_queryet method.
def list(self, request, *args, **kwargs):
"""
Method for Post listing. It can be accessed by anyone.
"""
serializer = PostListSerializer(self.filter_queryset(self.get_queryset()), many=True, context= .
{"request": request})
return Response(serializer.data)

map each post to the user who posted it by foreign key in django?

I want to connect each post with the logged in user who posted it.
models.py
from django.conf import settings
from django.db import models
# Create your models here.
class Campagin(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
title = models.CharField(max_length=120)
media = models.FileField()
description = models.TextField(max_length=220)
timestamp = models.DateTimeField(auto_now=False, auto_now_add=True)
updated = models.DateTimeField(auto_now=True, auto_now_add=False)
def __str__(self):
return self.title`
As you can see the posts were made by two different users, but the relation shows that it is made by the first user
this image shows the registered users..
Views.py
class NewCampagin(LoginRequiredMixin, CreateView):
template_name = 'campagin/new_campagin.html'
model = Campagin
fields = ['title','media','description']
def get_absolute_url(self):
return reverse('campagin:active_campagin')
Okay so CreateView allows you to specify the model and fields attributes to implicitly create a form for you. It's quite neat for quick form submissions but in your case, you will need to make some customizations before saving the Campaign object into the database (linking up the current logged in user).
As a result, you will need to create your own form first (create a file called forms.py which can be next to your views.py) and enter this code:
class CampaignForm(ModelForm): # Import ModelForm too.
def __init__(self, *args, **kwargs):
# We need to get access the currently logged in user so set it as an instance variable of CampaignForm.
self.user = kwargs.pop('user', None)
super(CampaignForm, self).__init__(*args, **kwargs)
class Meta:
model = models.Campaign # you need to import this from your models.py class
fields = ['title','media','description']
def save(self, commit=True):
# This is where we need to insert the currently logged in user into the Campaign instance.
instance = super(CampaignForm, self).save(commit=False)
# Once the all the other attributes are inserted, we just need to insert the current logged in user
# into the instance.
instance.user = self.user
if commit:
instance.save()
return instance
Now that we have our forms.py all ready to go we just need to modify your views.py:
class NewCampagin(LoginRequiredMixin, CreateView):
template_name = 'campagin/new_campagin.html'
form_class = forms.CampaignForm # Again, you'll need to import this carefully from our newly created forms.py
model = models.Campaign # Import this.
queryset = models.Campaign.objects.all()
def get_absolute_url(self):
return reverse('campagin:active_campagin') # Sending user object to the form, to verify which fields to display/remove (depending on group)
def get_form_kwargs(self):
# In order for us to access the current user in CampaignForm, we need to actually pass it accross.
# As such, we do this as shown below.
kwargs = super(NewCampaign, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
What's actually happening with my POST requests under the bonnet??
Note: This is just extra information for the sake of learning. You do
not need to read this part if you don't care about how your class
based view is actually handling your post request.
Essentially CreateView looks like this:
class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
"""
View for creating a new object instance,
with a response rendered by template.
"""
template_name_suffix = '_form'
Doesn't look that interesting but if we analyse BaseCreateView:
class BaseCreateView(ModelFormMixin, ProcessFormView):
"""
Base view for creating an new object instance.
Using this base class requires subclassing to provide a response mixin.
"""
def post(self, request, *args, **kwargs):
self.object = None
return super(BaseCreateView, self).post(request, *args, **kwargs)
we can see we are inheriting from two very important classes ModelFormMixin and ProcessFormView. Now the line, return super(BaseCreateView, self).post(request, *args, **kwargs), essentially calls the post function in ProcessFormView which looks like this:
def post(self, request, *args, **kwargs):
"""
Handles POST requests, instantiating a form instance with the passed
POST variables and then checked for validity.
"""
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
As you can see, your CreateView really just boils down to this small post function which simply gets a specified form and validates + saves it. There's 2 questions to ask at this point.
1) What does form = self.get_form() do since I didn't even specify my form?
2) What is self.form_valid(form) actually doing?
To answer the first question, self.get_form() essentially calls another function form_class = self.get_form_class() and this function is actually found in ModelFormMixin (the one where inherited from!):
def get_form_class(self):
"""
Returns the form class to use in this view.
"""
if self.fields is not None and self.form_class:
raise ImproperlyConfigured(
"Specifying both 'fields' and 'form_class' is not permitted."
)
if self.form_class:
return self.form_class
else:
if self.model is not None:
# If a model has been explicitly provided, use it
model = self.model
elif hasattr(self, 'object') and self.object is not None:
# If this view is operating on a single object, use
# the class of that object
model = self.object.__class__
else:
# Try to get a queryset and extract the model class
# from that
model = self.get_queryset().model
if self.fields is None:
raise ImproperlyConfigured(
"Using ModelFormMixin (base class of %s) without "
"the 'fields' attribute is prohibited." % self.__class__.__name__
)
# THIS IS WHERE YOUR FORM WAS BEING IMPLICITLY CREATED.
return model_forms.modelform_factory(model, fields=self.fields)
As you can see, this function is where your form was being implicitly created (see very last line). We needed to add more functionality in your case so we created our own forms.py and specified form_class in the views.py as a result.
To answer the second question, we need to look at the function (self.form_valid(form)) call's source code:
def form_valid(self, form):
"""
If the form is valid, save the associated model.
"""
# THIS IS A CRUCIAL LINE.
# This is where your actual Campaign object is created. We OVERRIDE the save() function call in our forms.py so that you could link up your logged in user to the campaign object before saving.
self.object = form.save()
return super(ModelFormMixin, self).form_valid(form)
So here we are simply saving the object.
I hope this helps you!
More information at https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-editing/#createview

Categories

Resources