I have a project with a Post model, that is basic posts. I want to create a link on each post page to be able to delete that post (with appropriate security).
There are a few questions on this on stack overflow, but I can't seem to find a complete, workable answer (I am using Django 1.7) that doesn't throw up errors when I implement it.
I have been able to implement a delete function which works ok, but need to add a POST form with CSRF token for validation, and also check that the user deleting it is the one that created it. I can't seem figure out how to add these two in.
So far, in my views.py:
def delete(request, id):
post = Post.objects.filter(pk=id).delete()
return HttpResponseRedirect(reverse('posts.views.all_posts'))
In urls.py:
url(r'^delete/(?P<id>\d+)/$','posts.views.delete'),
In html:
Delete
This all works, but there is no security - so appreciate guidance on how to add a form and checking.
Also, I've seen an answer that uses DeleteView, but couldn't get that one to work either.
Indeed, using a GET method to delete your objects makes you vulnerable to CSRF attacks.
DeleteView only deletes on POST, and shows a confirmation page on GET.
Your code should look something like this in views.py:
from django.views.generic import DeleteView
class PostDelete(DeleteView):
model = Post
success_url = reverse_lazy('posts.views.all_posts')
In urls.py:
url(r'^delete/(?P<pk>\d+)/$', PostDelete.as_view(),
name='entry_delete'),
Your form (without using a confirmation template. There is an example of confirmation template in the docs):
<form action="{% url 'entry_delete' object.pk %}" method="post">
{% csrf_token %}
<input type="submit" value="Delete" />
</form>
If you are not using a confirmation template, make sure to point the form's action attribute to the DeleteView (this is why).
To ensure the user deleting the post is the user that owns it, I like to use mixins. Assuming your Post model has a created_by foreign key pointing to User, you could write a mixin like:
from django.core.exceptions import PermissionDenied
class PermissionMixin(object):
def get_object(self, *args, **kwargs):
obj = super(PermissionMixin, self).get_object(*args, **kwargs)
if not obj.created_by == self.request.user:
raise PermissionDenied()
else:
return obj
Finally, your DeleteView should inherit from this mixin:
class PostDelete(PermissionMixin, DeleteView):
model = Post
success_url = reverse_lazy('posts.views.all_posts')
Related
I have a problem converting from a function-based view to a class-based view, function
VIEWS.PY
# login_required
def favourite_add(request, id):
post = get_object_or_404(Perfumes, id=id)
if post.favourites.filter(id=request.user.id).exists():
post.favourites.remove(request.user)
else:
post.favourites.add(request.user)
return HttpResponseRedirect(request.META['HTTP_REFERER'])
URLS.PY
urlpatterns = [
path('fav/<int:id>/', views.favourite_add, name='favourite_add'),
]
TEMPLATE.HTML
<div>
Add
</div>
In general, the goal is to get the id of a certain perfume on the page, and using the get_object_or_404 function, I'm pulling its object from the Perfumes database - the post variable. Next, I want to retrieve the id of the logged-in user and check if the id of the above user is in the favourites section of the post variable. If not then add, otherwise remove the user id to the favourites section of the post variable.
You should not do this through a GET request, as the safe methods section of the HTTP specifications [w3.org] says:
In particular, the convention has been established that the GET and HEAD methods SHOULD NOT have the significance of taking an action other than retrieval. These methods ought to be considered "safe".
GET and HEAD are thus supposed to have no side effects. You can for example work with a POST request:
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
class FavouriteView(LoginRequiredMixin, View):
def post(self, request, id):
perfume = get_object_or_404(Perfumes, id=id)
if request.user in perfume.favourites.all():
perfume.favourites.remove(request.user)
else:
perfume.favourites.add(request.user)
return HttpResponseRedirect(request.META['HTTP_REFERER'])
You then make a mini-form to make a POST request:
<form method="post" action="{% url 'favourite_add' perfume.id %}">
{% csrf_token %}
<button class="btn btn-outline-primary">Add</button>
</form>
Note: normally a Django model is given a singular name, so Perfume instead of Perfumes.
A function based one probably works fine for what you require.
Anyway, here is a view that should perform the same job as yours:
urls.py:
urlpatterns = [
path('fav/<int:id>/', views.FavouriteView.as_view(), name='favourite_add'),
]
views.py:
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
class FavouriteView(LoginRequiredMixin, View):
def get(self, *args, **kwargs):
post = get_object_or_404(Perfumes, id=self.kwargs.get('id'))
if post.favourites.filter(id=self.request.user.id).exists():
post.favourites.remove(self.request.user)
else:
post.favourites.add(self.request.user)
return HttpResponseRedirect(self.request.META['HTTP_REFERER'])
I recommend looking at something like https://ccbv.co.uk/ to help you understand class based views
I've seen this Q&A:
Django DeleteView without confirmation template
and this one:
Django CSRF token won't show
but that doesn't address the built-in intended functionality of the DeleteViewm CBV when issued a GET. From the docs (emphasis mine):
https://docs.djangoproject.com/en/2.1/ref/class-based-views/generic-editing/#django.views.generic.edit.DeleteView
"If this view is fetched via GET, it will display a confirmation page that should contain a form that POSTs to the same URL."
The problem is that as I understand it, the rendered template in response to a GET will not included the RequestContext necessary to include the {% csrf_token %} in the mentioned POST form. I worked around it for the time being by overriding the get() method so that it uses render() to return the page, since it automatically includes the appropriate context.
How do I maximally leverage the DeleteView? What am I doing wrong that I need to implement the following code in my view?
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return render(self.request,'mainapp/template_confirm_delete.html')
I am trying to show a newsletter form, and it is not shown in the page
This is my models.py
from django.db import models
# Create your models here.
class newsletter_user(models.Model):
email = models.EmailField()
date_added = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.email
This is my forms.py
from django import forms
from .models import newsletter_user
class newsletterForm(forms.ModelForm):
class Meta:
model = newsletter_user
fields = ['email']
def clean_email(self):
email = self.cleaned_data.get('email')
return email
This is my admin.py
from django.contrib import admin
from .models import newsletter_user
# Register your models here.
class newsletterAdmin(admin.ModelAdmin):
list_display = ('email','date_added',)
admin.site.register(newsletter_user,newsletterAdmin)
This is the views.py
from django.shortcuts import render
from .models import newsletter_user
from .forms import newsletterForm
# Create your views here.
def newsletter_subscribe(request):
form = newsletterForm(request.POST or none)
if form.is_valid():
instance = form.save(commit=false)
if newsletter_user.objects.filter(email=instance.email).exists():
print("already exists")
else:
instance.save()
context = {'form':form,}
template = "/blog/templates/footer.html"
return render(request, template, context)
This is the html
<form method="post" action=''>
<div class = "input-group">
{{form}} {% csrf_token %}
<span class = "input-group-btn">
<button class="btn btn-default" type="submit">Subscribe</button>
</span>
</div>
</form>
This is my urls.py
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', views.BlogIndex.as_view(), name='home'),
url(r'^(?P<slug>[-\w]+)/$', views.BlogDetail.as_view(), name='entry_detail'),
url(r'^ckeditor/', include('ckeditor_uploader.urls')),
url(r'^footer/$', subscribe_views.newsletter_subscribe, name='subscribe'),
]
My Project directory
The button is shown
But the form is not shown..
This is my source in web browser RIGHT-CLICK->VIEW SOURCE
The url router will send the request to the first matching view. That is the only one that is called, and that view has to provide the context data that the template consumes. (You can also write your own context processor to insert context that you need everywhere.)
Since another pattern also matches /footer/, your request is clearly handled by some other view.
url(r'^(?P<slug>[-\w]+)/$', views.BlogDetail.as_view(), name='entry_detail'),
If the other view doesn't provide form into the context, there's nothing for Django to render.
Your view function newsletter_detail() is not called from other views, so that context is not used. (Using the undefined none there would have caused a run time error, which shows that the code was never evaluated.)
Catch-all routes such as entry_detail should either be used as the last url route, or be made more specific. Something like r'^/blog/(?P<slug>[-\w]+)/$', for instance, which will not match /footer/.
For a simple "subscribe" form in the footer, I recommend writing it as just html, and set up a route /subscribe/ to handle POST requests. There's not anything to gain by using Django's form framework for such a simple case (Just one field).
The django docs has an example of how you can implement something like this.
You footer.html template fragment should not require any context that is not automatically inserted by a context processor. Django's CsrfViewMiddleware provides the {% csrf_token %}, so that's an example of something you can use in template fragments such as a footer.
If you need some complicated form in your footer, you can write custom middleware to insert a Django Form instance in every context, (but you should probably give it a less generic name than form).
You may need to make some changes in your view somewhat like this,
def newsletter_subscribe(request):
if request.method == 'POST':
form = newsletterForm(request.POST)
if form.is_valid():
instance = form.save(commit=false)
if newsletter_user.objects.filter(email=instance.email).exists():
print("already exists")
else:
instance.save()
else:
form = newsletterForm()
context = {'form':form,}
template = "/blog/templates/footer.html"
return render(request, template, context)
You only need to initialise the form with request.POST , if request method is actually "POST". Else, just initialise a blank form.
What's the best way to add a "cancel" button to a generic class-based view in Django?
In the example below, I would like the cancel button to take you to success_url without deleting the object. I have tried adding a button <input type="submit" name="cancel" value="Cancel" /> to the template. I can detect if this button was pressed by overriding the post method of the AuthorDelete class, but I can't work out how to redirect from there.
Example myapp/views.py:
from django.views.generic.edit import DeleteView
from django.core.urlresolvers import reverse_lazy
from myapp.models import Author
class AuthorDelete(DeleteView):
model = Author
success_url = reverse_lazy('author-list')
def post(self, request, *args, **kwargs):
if request.POST["cancel"]:
return ### return what? Can I redirect from here?
else:
return super(AuthorDelete, self).post(request, *args, **kwargs)
Example myapp/author_confirm_delete.html:
<form action="" method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<input type="submit" value="Confirm" />
<input type="submit" name="cancel" value="Cancel" />
</form>
(Examples adapted from the docs)
Your approach of overriding the post method and checking to see if the cancel button was pressed is ok. You can redirect by returning an HttpResponseRedirect instance.
from django.http import HttpResponseRedirect
class AuthorDelete(DeleteView):
model = Author
success_url = reverse_lazy('author-list')
def post(self, request, *args, **kwargs):
if "cancel" in request.POST:
url = self.get_success_url()
return HttpResponseRedirect(url)
else:
return super(AuthorDelete, self).post(request, *args, **kwargs)
I've used get_success_url() to be generic, its default implementation is to return self.success_url.
Why don't you simply put a "Cancel" link to the success_url instead of a button? You can always style it with CSS to make it look like a button.
This has the advantage of not using the POST form for simple redirection, which can confuse search engines and breaks the Web model. Also, you don't need to modify the Python code.
If using CBV's you can access the view directly from the template
Cancel
Note: you should access it through the getter in case it has been subclassed.
This is noted in the ContextMixin docs
The template context of all class-based generic views include a view
variable that points to the View instance.
Having an element of type button, will not send a POST request. Therefore, you can use this to do a http redirection like this:
<button type="button" onclick="location.href='{{ BASE_URL }}replace-with-url-to-redirect-to/'">Cancel</button>
Do you even need the get_success_url, why not just use:
Cancel
and go to any other url you want?
In a mini blog app, I want to create a delete function, so that the owner of the blog can delete his entries (and only his entries).
I guess that the only methods for doing do, is using a form.
Though my the deletion code seems clear and correct, it doesn't work.
My code:
def delete_new(request,id):
u = New.objects.get(pk=id).delete()
if request.method == 'POST':
form = DeleteNewForm(request.POST)
form.u.delete()
form.save()
return render_to_response('news/deleteNew.html', {
'form': form,
},
context_instance=RequestContext(request))
and in the template:
<a href='/news/delete_new/{{object.id}}/'> Delete</a> <br />
Is this a correct approach? I mean, creating a form for this?
also, the only way to take the blog post associated with the deletion link is having an id as a parameter. Is it right? I mean, maybe any user can type another id, in the url, and delete another entry (eventually not one of his)
You need to use a form, or you're vulnerable to CSRF attacks. You're also deleting the model before you've checked whether the request was a GET or a POST.
Create a simple ModelForm:
from django import forms
from .models import New
class DeleteNewForm(forms.ModelForm):
class Meta:
model = New
fields = []
In your views.py in the same Django app:
from django.shortcuts import render, get_object_or_404
from .forms import DeleteNewForm
from .models import New
def delete_new(request, new_id):
new_to_delete = get_object_or_404(New, id=new_id)
#+some code to check if this object belongs to the logged in user
if request.method == 'POST':
form = DeleteNewForm(request.POST, instance=new_to_delete)
if form.is_valid(): # checks CSRF
new_to_delete.delete()
return HttpResponseRedirect("/") # wherever to go after deleting
else:
form = DeleteNewForm(instance=new_to_delete)
template_vars = {'form': form}
return render(request, 'news/deleteNew.html', template_vars)
In general, for deleting objects you should rather use POST (or DELETE) HTTP methods.
If you really want to use HTTP GET for your example, here is what you need to fix:
If you have url pointing to some url like yours: <a href='/news/delete_new/{{object.id}}/'> Delete</a> then you can simply write view that will check if object belongs to logged in user and delete this entry if yes, like in code you have already written:
def delete_new(request,id):
#+some code to check if New belongs to logged in user
u = New.objects.get(pk=id).delete()
To check if New objects belogs to some user you need to create realation between User and New (like created_by = models.ForeignKey(User) in New model).
You can get logged in user this way: request.user
I hope I got your point correctly and my answer helps you somehow.
PS: You can also consider using {% url %} tag instead of writing urls directly in your templates.