Django: Permission per instance - python

At the moment, I plan to write a website where users can write blog posts. I haven't started with the implementation yet because I am new to Django and want to get an overview in how I should solve problems and structure the overall project.
My problem:
The users aren't allowed to use Django's admin page (/admin). The users should only interact with the website that I write.
The users at my website will be able to create blog posts. Now, I have to ensure that only the creator of the blog post should be able to edit/delete his own post. All the other users should only be able to read this post.
So, I need permissions per user/instance. I know that there are some full-blown permission-systems like django-guardian, but I would like to prefer to solve it on my own. Also to better understand the Django framework.
I`ve already read the Django docs, but I am overwhelmed.
I guess this is such a common problem that I there is also a common solution to this problem. Can someone help me where I should look at, where to start?
My idea so far (please comment):
In create a page like blog/5/edit that is only accessible for the user that created the blog post number 5. This should be easily within the view function, right? On this page there is a Form to edit the blog post.
Something like this for the view (pseudo-code!)
if request.user != blogpost.user:
redirect/error page
else:
show Form/process Form
Is this secure? I haven't found such a solution so far while searching the internet. However, spontaneously and as a Django beginner, I think this could already by enough.

Yes, you can do that. For that purpose I recommend to create an custom decorator like so:
decorators.py:
from django.core.exceptions import PermissionDenied
from .models import YourBlogPostModel
def user_is_blog_post_user(function):
def wrap(request, *args, **kwargs):
post = YourBlogPostModel.objects.get(pk=kwargs['blog_post_id'])
if post.user == request.user:
return function(request, *args, **kwargs)
else:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
You can then use it in conjunction with other decorators.
views.py:
#login_required
#user_is_blog_post_user
def function_based_view(request, blog_post_id):
# ...
Take a look here for a more detailed explanation.
If you are using CVB you can either create an own Mixin or decorate the class.
class UserIsBlogPostAdmin(object):
"""Verify that the current user is the user of the blog post."""
def dispatch(self, request, *args, **kwargs):
blog_post = get_object_or_404(YourBlogPostModel, pk=args[0])
if blog_post.user != request.user
raise Http404
return super().dispatch(request, *args, **kwargs)

Related

Wagtail Admin: How to control where user is redirected after submitting changes for moderation

On a Wagtail site I've built, I have a model type that is editable by authenticated users who do not have full admin privileges. They can only save as a draft or submit the changes for moderation. The problem I'm having is that Wagtail is inconsistent about where it redirects after executing these two actions. Saving the Draft takes the user back to the edit screen they were just on, with a note saying the draft was saved (good). Submitting for Moderation returns the user to the admin browse view of the parent page, which shows all of the sibling nodes in a list. They can't edit the vast majority of items on that list, so I think this is confusing for a non-admin user. I would like to have the "Submit for Moderation" action detect whether the user belongs to a group other than admin (or, failing that, whether the page has unpublished changes, as in my code example below) and, if so, redirect them back to the edit screen just like "Save as Draft" does.
I tried this in my model definition and it didn't work:
def save(self, *args, **kwargs):
#do some field value manipulations here before saving
super().save(*args, **kwargs)
if self.id:
if self.has_unpublished_changes:
return HttpResponseRedirect('/admin/pages/' + str(self.id) + '/edit/')
There's probably some sort of Wagtail admin action that I need to hook into and override, rather than trying to accomplish this in models.py, but I don't have much experience with this, so I need a better understanding of what to change and where.
Set up an after_create_page and after_edit_page hook: https://docs.wagtail.io/en/stable/reference/hooks.html#after-create-page
To do this, add a wagtail_hooks.py file in one of the apps in your project. This is where you can define functions to be called after creating or editing a page through the admin, such as:
from wagtail.core import hooks
#hooks.register('after_create_page')
def redirect_after_page_create(request, page):
if not request.user.is_superuser:
return HttpResponseRedirect('/admin/pages/' + str(page.id) + '/edit/')
#hooks.register('after_edit_page')
def redirect_after_page_edit(request, page):
if not request.user.is_superuser:
return HttpResponseRedirect('/admin/pages/' + str(page.id) + '/edit/')

Prevent repopulation and/or resubmit of Django form after using the back button

The Problem
We have the following setup.
Pretty standard Django class based view (inherits from CreateView, which is what I'll call it form now on).
After a successful POST and form validation, the object is created, and the user is redirect_to'd the DetailView of the created record.
Some users decide that they are not happy with the data they entered. They press the back button.
The HTML generated by the CreateView is fetched form browser cache, and repopulated with the data they entered.
To the user, this feels like an edit, so they change the data and submit again.
The result is 2 records, with minor differences.
What have we tried?
At first I thought the Post-Redirect-Get (PRG) pattern that Django uses was supposed to prevent this. After investigating, it seems that PRG is only meant to prevent the dreaded "Do you want to resubmit the form?" dialog. Dead end.
After hitting the back button, everything is fetched from cache, so we have no chance of interacting with the user from our Django code. To try and prevent local caching, we have decorated the CreateView with #never_cache. This does nothing for us, the page is still retrieved form cache.
What are we considering?
We are considering dirty JavaScript tricks that do an onLoad check of window.referrer, and a manual clean of the form and/or notice to user if the referrer looks like the DetailView mentioned earlier. Of course this feel totally wrong. Then again, so do semi-duplicate records in our DB.
However, it seems so unlikely that we are the first to be bothered by this that I wanted to ask around here on StackOverflow.
Ideally, we would tell the browser that caching the form is a big NO, and the browser would listen. Again, we already use #never_cache, but apparently this is not enough. Happens in Chrome, Safari and Firefox.
Looking forward to any insights! Thanks!
Maybe don't process the POST request when it's coming from a referrer other than the same page?
from urllib import parse
class CreateView(...):
def post(self, *args, **kwargs):
referer = 'HTTP_REFERER' in self.request.META and parse.urlparse(self.request.META['HTTP_REFERER'])
if referer and (referer.netloc != self.request.META.get('HTTP_HOST') or referer.path != self.request.META.get('PATH_INFO')):
return self.get(*args, **kwargs)
...
I know I'm late to this party but this may help anybody else looking for an answer.
Having found this while tearing my hair out over the same problem, here is my solution using human factors rather than technical ones. The user won't use the back button if after submitting from a CreateView, he ends up in an UpdateView of the newly created object that looks exactly the same apart from the title and the buttons at the bottom.
A technical solution might be to create a model field to hold a UUID and create a UUID passed into the create form as a hidden field. When submit is pressed, form_valid could check in the DB for an object with that UUID and refuse to create what would be a duplicate (unique=True would enforce that at DB level).
Here's example code (slightly redacted to remove stuff my employer might not want in public). It uses django-crispy-forms to make things pretty and easy. The Create view is entered from a button on a table of customers which passes the customer account number, not the Django id of its record.
Urls
url(r'enter/(?P<customer>[-\w]+)/$', JobEntryView.as_view(), name='job_entry'),
url(r'update1/(?P<pk>\d+)/$', JobEntryUpdateView.as_view(), name='entry_update'),
Views
class JobEntryView( LoginRequiredMixin, CreateView):
model=Job
form_class=JobEntryForm
template_name='utils/generic_crispy_form.html' # basically just {% crispy form %}
def get_form( self, form_class=None):
self.customer = get_object_or_404(
Customer, account = self.kwargs.get('customer','?') )
self.crispy_title = f"Create job for {self.customer.account} ({self.customer.fullname})"
return super().get_form( form_class)
def form_valid( self, form): # insert created_by'class
#form.instance.entered_by = self.request.user
form.instance.customer = self.customer
return super().form_valid(form)
def get_success_url( self):
return reverse( 'jobs:entry_update', kwargs={'pk':self.object.pk, } )
# redirect to this after entry ... user hopefully won't use back because it's here already
class JobEntryUpdateView( LoginRequiredMixin, CrispyCMVPlugin, UpdateView):
model=Job
form_class=JobEntryForm
template_name='utils/generic_crispy_form.html'
def get_form( self, form_class=None):
self.customer = self.object.customer
self.crispy_title = f"Update job {self.object.jobno} for {self.object.customer.account} ({self.object.customer.fullname})"
form = super().get_form( form_class)
form.helper[-1] = ButtonHolder( Submit('update', 'Update', ), Submit('done', 'Done', ), )
return form
def get_success_url( self):
print( self.request.POST )
if self.request.POST.get('done',None):
return reverse('jobs:ok')
return reverse( 'jobs:entry_update',
kwargs={'pk':self.object.pk, } ) # loop until user clicks Done

Django -- Allowing Users To Only View Their Own Page

I'm nearing what I think is the end of development for a Django application I'm building. The key view in this application is a user dashboard to display metrics of some kind. Basically I don't want users to be able to see the dashboards of other users. Right now my view looks like this:
#login_required
#permission_required('social_followup.add_list')
def user_dashboard(request, list_id):
try:
user_list = models.List.objects.get(pk=list_id)
except models.List.DoesNotExist:
raise Http404
return TemplateResponse(request, 'dashboard/view.html', {'user_list': user_list})
the url for this view is like this:
url(r'u/dashboard/(?P<list_id>\d+)/$', views.user_dashboard, name='user_dashboard'),
Right now any logged in user can just change the list_id in the URL and access a different dashboard. How can I make it so a user can only view the dashboard for their own list_id, without removing the list_id parameter from the URL? I'm pretty new to this part of Django and don't really know which direction to go in.
Just pull request.user and make sure this List is theirs.
You haven't described your model, but it should be straight forward.
Perhaps you have a user ID stored in your List model? In that case,
if not request.user == user_list.user:
response = http.HttpResponse()
response.status_code = 403
return response
I solve similiar situations with a reusable mixin. You can add login_required by means of a method decorator for dispatch method or in urlpatterns for the view.
class OwnershipMixin(object):
"""
Mixin providing a dispatch overload that checks object ownership. is_staff and is_supervisor
are considered object owners as well. This mixin must be loaded before any class based views
are loaded for example class SomeView(OwnershipMixin, ListView)
"""
def dispatch(self, request, *args, **kwargs):
self.request = request
self.args = args
self.kwargs = kwargs
# we need to manually "wake up" self.request.user which is still a SimpleLazyObject at this point
# and manually obtain this object's owner information.
current_user = self.request.user._wrapped if hasattr(self.request.user, '_wrapped') else self.request.user
object_owner = getattr(self.get_object(), 'author')
if current_user != object_owner and not current_user.is_superuser and not current_user.is_staff:
raise PermissionDenied
return super(OwnershipMixin, self).dispatch(request, *args, **kwargs)
You need to have some information stored about what list or lists a user can access, and then include that in the user_list lookup. Let's assume the simple case where List has a single owner, a foreign key to the User model. That's a many-to-one relationship between lists and users; no list is owned by more than one user, but a user can have multiple lists. Then you want something like this:
try:
user_list = models.List.objects.get(pk=list_id, owner=request.user)
except models.List.DoesNotExist:
raise Http404
Whether to return 404 or 403 is to some extent a matter of opinion; the definition for 403 says:
If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled, it SHOULD describe the reason for the refusal in the entity. If the server does not wish to make this information available to the client, the status code 404 (Not Found) can be used instead.
http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
If you do return a 404, you can use the django shortcut function get_object_or_404 instead of the explicit try/except - there's nothing wrong with doing it explicitly, but the need is common enough that there's a convenience function to do it.

How can I access the URL params and use them if I'm using a formwizard in Django?

I'm using Django 1.4. I've spent quite a while googleing and looking at docs on the django page, https://docs.djangoproject.com/en/1.4/ref/contrib/formtools/form-wizard/ etc. Few people have asked the question and no-one happens to have answered it.
What I need to do is:
use the slug paramater from the urls.py (as follows):
FORMS=[...]
url(r'^url/(?P<slug>[\w-]+)/form/$', WizardWizard.as_view(FORMS)),
in the views.py (as follows):
class WizardWizard(SessionWizardView):
template_name = "template.html"
extra_context = Model.objects.filter(slug=slug) # HERE!
def done(self, form_list, **kwargs):
...
Solved this problem too. FormWizard instance contains self.args and self.kwargs which are set to args and kwargs passed to view from URLconf. You can see it in django.views.generic.base.dispatch
After being stuck for days the answer is fairly simple. If you're reading this, make sure you're using Class based generic views first or it's a different problem.
You can find the relevant information on the following link, it's not clear this is how you assign extra context (not with the extra_context var):
https://docs.djangoproject.com/en/1.4/ref/contrib/formtools/form-wizard/#django.contrib.formtools.wizard.views.WizardView.get_context_data
example code (slightly different from theirs) that sets a context var for ALL of you formwizard pages:
def get_context_data(self, form, **kwargs):
context = super(PaperworkWizard, self).get_context_data(form=form, **kwargs)
#this line makes camp available as a var in the template context.
#it sets it to an OBJECT RETRIEVED USING THE SLUG FROM THE URL.
context.update({'camp': Camp.objects.get(slug=self.kwargs.get("slug"))})
return context
You need to put this code in the subclass you've made of the SessionWizardView (or CookieWizardView). In the context of the question, the WizardWizard class in the views.py file.
The problem with doing this any other way, is that the self.args and self.kwargs are set when the dispatch function is run. NOTE: You can't set the context by setting the extra_context variable This is where I fell down.
credit to nkryptic from the #django channel on freenode. It's a fantastic place to head if you are still stuck after this answer. I wish everyone the best of luck.

What are some predefined Django methods used in class based views

I am new to django and in my current job i had to edit the existing code.
Now i am not able to find in the documentation where i can find all methods which can be over ridden and what they are suppose to do.
In mY views file someone has defined these functions but i don't know what they are supposed to do.
Like
def get_context_data(self, **kwargs):
def get(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
def get_success_url(self):
def form_valid(self, form):
Now i am not sure if he as write his new methods or He is overriding the buil in methods because he is not calling them anywhere.
can someone guide where are these methods defined in documentation so that i can see what are some other methods and what they do
I found this resource really uesful, as it saves you having to manually trace inheritance hierarchies to check which methods a class has.
http://ccbv.co.uk/
The official documentation is here, depending on your django version:
https://docs.djangoproject.com/en/1.3/ref/class-based-views/
https://docs.djangoproject.com/en/1.4/ref/class-based-views/
https://docs.djangoproject.com/en/dev/ref/class-based-views/
get_context_data returns the data that will be accessible in your template
get is all the actions that are performed when a page is loaded
post is all the actions that are performed when data is posted to a url
get_success_url returns the url that the user will be directed to after a post was succesfully made
form_valid here you can add extra actions to the form validation

Categories