I am writing a FormView which will add for example a comment on Person object.
I want to check if current user wrote a comment for this Person and if he did I'd like to raise Http404.
Question: What is the best place for this validation? I don't want to call validation in get_context_data and form_valid. Is dispatch method a good place for this logic?
Remember that form_valid will only be called when you POST the form so that won't work as GET requests will still render. You could therefore put it in the get method for the FormView which would prevent the view and template loading the initial form. The drawback is that people could technically still POST to that URL if they really wanted to.
As you mentioned, I would put it in the dispatch method. It is very early in the cycle of the FormView so you avoid unnecessary processing.
def dispatch(self, request, *args, **kwargs):
# I'm just guessing your Comment/Person models
user = self.request.user
try:
person = Person.objects.get(user=self.request.user)
except:
raise Http404("No user exists")
if Comment.objects.filter(content_object=person).exist():
raise Http404("Comment already exists")
return super(MyFormView, self).dispatch(request, *args, **kwargs)
EDIT This is a good link describing the various ways you can decorate your class based view to add permission and user checking
I wrote this mixin
class PermissionCheckMixin(object):
def __init__(self, perm=None, obj=None):
self.perm = perm
self.obj = obj
def dispatch(self, request, *args, **kwargs):
if request.user.is_anonymous():
if request.is_ajax():
return JSONResponseForbidden()
else:
return HttpResponseForbidden()
elif request.user.is_authenticated():
if self.perm:
if request.user.has_perm(self.perm, self.obj):
return super(PermissionCheckMixin, self).dispatch(request, *args, **kwargs)
else:
if request.is_ajax():
return JSONResponseForbidden()
else:
return HttpResponseForbidden()
else:
if request.is_ajax():
return JSONResponseForbidden()
else:
return HttpResponseForbidden()
And use it like this:
class TestFormView(PermissionCheckMixin, FormView):
...
You can easily adapt this mixin, somehow like this:
def __init__(self, pk):
self.person_pk = pk
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated():
if request.user.pk == self.person_pk:
return HttpResponseNotFound()
Related
Env: Python 3.6, Django 3.0, DRF 3.11, JWT Authorization
Hello everybody.
I have a simple class in my API where I want to check user permissions in each method get, post etc. I planned to check user privileges at dispatch but it doesn't work. Simplify code, my current class looks more or less like this:
class ExampleClassName(APIView):
can_do_sth = False
def dispatch(self, request, *args, **kwargs):
print(request.user) # Here is AnonymousUser
if request.user.username == "GreatGatsby":
self.can_do_sth = True
return super(ExampleClassName, self).dispatch(request, *args, **kwargs)
def get(self, request):
print(request.user) # Here is correctly Logged user
if self.can_do_sth:
return Response("You can do it")
return Response("You can't")
def post(self, request):
print(request.user) # Here is correctly Logged user
if self.can_do_sth:
return Response("You can do it")
return Response("You can't")
How can I pass request.user to dispatch method?
ok solved
initialize_request - is doing what I expected. so right dispatch should be:
def dispatch(self, request, *args, **kwargs):
req = self.initialize_request(request, *args, **kwargs)
print(req.user) # Now I have logged user data
if req.user.username == "GreatGatsby":
self.can_do_sth = True
return super(ExampleClassName, self).dispatch(request, *args, **kwargs)
Task closed unless you have any other idea how to do it in different way. If you do - please share :)
I am using a generic view to render my blog post item:
class PostUpdateView(UpdateView, LoginRequiredMixin):
model = Post
# etc
I have a model method on the Post model that results in a boolean True or False:
#property
def can_edit(self):
return self.displays_set.count() == 0
If can_edit is False for the Post object, how can I refactor the view to redirect from my UpdateView to a different DetailView?
Override the dispatch method, and check obj.can_edit there. That way the object will be checked for get and post requests.
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
def dispatch(self, *args, **kwargs):
obj = self.get_object()
if not obj.can_edit:
return redirect('/readonly-view/')
return super().dispatch(*args, **kwargs)
With this solution, get_object() is called twice so there is a duplicate SQL query. However this is probably worth it to keep the code simple.
I would say that override dispatch method is best solution,
but if you want to avoid extra database hit, then you need to override get and post methods
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not obj.can_edit:
return redirect('/readonly-view/')
return self.render_to_response(self.get_context_data())
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not obj.can_edit:
return redirect('/readonly-view/')
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
I have following models:
class BankAccount(models.Model):
owner = models.ForeignKey(User)
class MoneyTransfer(models.Model):
sender = models.ForeignKey(BankAccount)
and url:
url(r'^accounts/(?P<pk>\w+)/send_transfer$', SendTransfer.as_view(), name='SendTransfer')
that means "I want to send money from Account with id=pk"
This is my view:
class SendTransfer(View):
form_class = SendTransferForm
template_name = 'dashboard/send_transfer.html'
def get(self, request, *args, **kwargs):
instance = BankAccount.objects.get(id=self.kwargs['pk'])
if instance.is_legal():
if instance.organization.owners.all().filter(user__id=self.request.user.id).count() == 0:
return None
else:
if instance.citizen.user.id != self.request.user.id:
return None
return render(self.request, self.template_name, self.get_context_data())
def post(self, request, *args, **kwargs):
sender = BankAccount.objects.get(id=kwargs['pk'])
form = self.form_class(sender, self.request.user, request.POST)
if form.is_valid():
MoneyTransfer.objects.create(sender=sender,
receiver=BankAccount.objects.get(id=self.request.POST['receiver']),
total=float(self.request.POST['total']),
when=timezone.localtime(timezone.now()),
comment=self.request.POST['comment'])
return redirect('AccountDetail', kwargs['pk'])
data = self.get_context_data()
data['form'] = form
return render(request, self.template_name, data)
def get_context_data(self):
account = BankAccount.objects.get(id=self.kwargs['pk'])
return {'form': SendTransferForm(account, self.request.user),
'user': self.request.user,
'account': account}
I think there's a lot of redudant code for CBV. What can I do for shorting it?
UPD
my current code:
class SendTransfer(SingleObjectMixin, FormView):
model = BankAccount
form_class = SendTransferForm
template_name = 'dashboard/send_transfer.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super(SendTransfer, self).dispatch(request, *args, **kwargs)
def get_object(self, queryset=None):
obj = super(SendTransfer, self).get_object(queryset)
if obj.is_legal():
if not obj.organization.owners.filter(user=self.request.user).exists():
raise Http404
else:
if obj.citizen.user != self.request.user:
raise Http404
return obj
def form_valid(self, form):
data = form.cleaned_data
MoneyTransfer.objects.create(sender=self.object,
receiver=data['receiver'], # ModelChoiceField in the form
total=data['total'], # FloatField in the form, etc.
when=timezone.localtime(timezone.now()),
comment=data['comment'])
return redirect('AccountDetail', self.object.pk)
last line of dispatch() method raises TypeError: init() takes at least 3 arguments (1 given)
CBVs are designed for code reuse. If you don't yet have another class that could benefit of code you posted, the actual amount of code is almost identical, be that a CBVs or a plain function.
But the more pythonic and Django-ish (from my biased POV) way would be to:
Inherit your class from the FormView instead of the View. That eases the form management a bit.
Add a SingleObjectMixin to get the object from url kwargs for free.
Move your object validation to the get_object() method. It's a good practive to raise 404 if your object doesn't validate.
Refactor out the get_context_data() as you already have all that data in your context (request, form and object)
Instead of relying on the self.request.POST, clean your data through the form.
class SendTransfer(SingleObjectMixin, FormView):
model = BankAccount
form_class = SendTransferForm
template_name = 'dashboard/send_transfer.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super(SendTransfer).dispatch(request, *args, **kwargs)
def get_object(self, queryset=None):
obj = super(SendTransfer, self).get_object(queryset)
if obj.is_legal():
if not obj.organization.owners.filter(user=self.request.user).exists():
raise Http404
else:
if obj.citizen.user != self.request.user:
raise Http404
return obj
def form_valid(self, form):
data = form.cleaned_data
MoneyTransfer.objects.create(sender=self.object,
receiver=data['receiver'], # ModelChoiceField in the form
total=data['total'], # FloatField in the form, etc.
when=timezone.localtime(timezone.now()),
comment=data['comment'])
return redirect('AccountDetail', self.object.pk)
Some of your code has gone thanks to the CBV magic, some just has moved to another methods. Take a look, I'd welcome your comments.
I am making a basic app to teach beginners. Each user can write notes, but I want to make it so that a user cannot view or update a different user's notes.
I have the following view, but I had to repeat myself.
from django.core.exceptions import PermissionDenied
...
class NoteUpdate(LoginRequiredMixin, UpdateView):
...
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.owner != self.request.user:
raise PermissionDenied
return super(NoteUpdate, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.owner != self.request.user:
raise PermissionDenied
return super(NoteUpdate, self).post(request, *args, **kwargs)
I feel like there is probably a way to do this without repeating myself. Yeah, I could write a method like this and call it from both:
def check_permission(self):
if self.object.owner != self.request.user:
raise PermissionDenied
But what I really mean is am I overriding the wrong methods? Is there a more traditional way to do this? It feels a little weird overriding .get() and .post()
To answer your question: Overriding .get() and .post() is fine, since for security and integrity reasons, you would want both your get() and post() views to validate before displaying and especially modifying data. Now, if you want to refactor doing this in get or post, there are 2 easy ways of doing this:
Primary (Model Method):
models.py
class Model(models.Model):
owner = models.ForeignKey(User)
...
def deny_if_not_owner(self, user):
if self.owner != user:
raise PermissionDenied
return self.owner
views.py
class NoteUpdate(LoginRequiredMixin, UpdateView):
...
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.deny_if_not_owner(request.user)
return super(NoteUpdate, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.deny_if_not_owner(request.user)
return super(NoteUpdate, self).post(request, *args, **kwargs)
///////
Alternative (Mixin):
Creating a Mixin would allow you to easily add this code to many classes if you see yourself using this validation again in the future.
class DenyWrongUserMixin(object):
def get(self):
if self.object.owner != self.request.user:
raise PermissionDenied
return super(DenyWrongUserMixin, self).get(*args, **kwargs)
def post(self):
if self.object.owner != self.request.user:
raise PermissionDenied
return super(DenyWrongUserMixin, self).post(*args, **kwargs)
and then:
class NoteUpdate(LoginRequiredMixin, DenyWrongUserMixin, UpdateView):
...
def get(self, request, *args, **kwargs):
...
def post(self, request, *args, **kwargs):
...
You can override the get method or the get_queryset method. The get_queryset will raise a 404 if the logged in user is not the owner.
def get_queryset(self):
qs = super(NoteUpdate, self).get_queryset()
return qs.filter(owner=self.request.user)
or you can just override the get method since it will be called first and then raise PermissionDenied, so no reason to override the post method as well.
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.owner != self.request.user:
raise PermissionDenied
return super(NoteUpdate, self).get(request, *args, **kwargs)
Then you can create a mixin and extend your views from the mixin to avoid the duplication.
I'm trying to use validation with the request.user object to restrict updates to some rows for specific users within the django admin site. I get the impression I need to override the ModelAdmin change_view method to pass the request object to the form. I've looked at the change_view method in django.contrib.admin.options, but as someone very new to django, am having trouble understanding where in the change_view method I need to make these modifications. Any pointers in the right direction would be great.
class IssuesAdmin(admin.ModelAdmin):
def change_view(self, request, object_id, extra_context=None):
#modify lines to pass request to form
form = IssuesAdminForm
class IssuesAdminForm(forms.ModelForm):
class Meta:
model = Issues
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(IssuesAdminForm, self).__init__(*args, **kwargs)
def clean_product(self):
if self.request.user.name=='someone'
return self.cleaned_data["product"]
else:
raise forms.ValidationError("Nope!")
class IssuesAdmin(admin.ModelAdmin):
def change_view(self, request, object_id, extra_context=None): #remember to edit also add_view()... etc
self.form.request = request
form = IssuesAdminForm
class IssuesAdminForm(forms.ModelForm):
class Meta:
model = Issues
def __init__(self, *args, **kwargs):
self.request = # do what you need ;)
super(IssuesAdminForm, self).__init__(*args, **kwargs)
def clean_product(self):
if self.request.user.name=='someone'
return self.cleaned_data["product"]
else:
raise forms.ValidationError("Nope!")