Redirect from Generic View DetailView in Django - django

I'm using Django's class based DetailView generic view to look up an object for display. Under certain circumstances, rather than displaying the object, I wish to back out and issue a HTTP rediect instead. I can't see how I go about doing this. It's for when a user hits an object in my app, but without using the canonical URL. So, for example, on StackOverflow URLs take the form:
http://stackoverflow.com/<content_type>/<pk>/<seo_friendly_slug>
eg:
http://stackoverflow.com/questions/5661806/django-debug-toolbar-with-django-cms-and-django-1-3
You can actually type anything as the seo_friendly_slug part and it will redirect you to the correct canonical URL for the object looked up via the PK.
I wish to do the same in my DetailView. Retrieve the object, check that it's the canonical URL, and if not redirect to the item's get_absolute_url URL.
I can't return an HttpResponseRedirect in get_object, as it's expecting the looked up object. I can't seem to return it from get_context_data, as it's just expecting context data.
Maybe I just need to write a manual view, but I wondered if anyone knew if it was possible?
Thanks!
Ludo.

This isn't a natural fit for DetailView. To do this you need to override the get method of BaseDetailView, which looks like:
class BaseDetailView(SingleObjectMixin, View):
def get(self, request, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
So in your class you'd need to provide a new get method which did the URL check between fetching the object and setting up the context. Something like:
def get(self, request, **kwargs):
self.object = self.get_object()
if self.request.path != self.object.get_absolute_url():
return HttpResponseRedirect(self.object.get_absolute_url())
else:
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
As you end up overriding so much of the functionality it becomes questionable whether it's worth actually using a generic view for this, but youknow.

Developing on Rolo's answer and comments, I came up with the following generic view to serve this purpose:
from django import http
from django.views import generic
class CanonicalDetailView(generic.DetailView):
"""
A DetailView which redirects to the absolute_url, if necessary.
"""
def get_object(self, *args, **kwargs):
# Return any previously-cached object
if getattr(self, 'object', None):
return self.object
return super(CanonicalDetailView, self).get_object(*args, **kwargs)
def get(self, *args, **kwargs):
# Make sure to use the canonical URL
self.object = self.get_object()
obj_url = self.object.get_absolute_url()
if self.request.path != obj_url:
return http.HttpResponsePermanentRedirect(obj_url)
return super(CanonicalDetailView, self).get(*args, **kwargs);
This is used in the same manner as the normal DetailView, and should work for any model which implements get_absolute_url correctly.

Related

DetailView redirect not working in django

I keep getting a reverse error when i try to redirect from a DetailView and CreateView. I keep getting object has no attribute pk. I have equally tried using : args=[str(self.id)]) but i still get the error.
class check (DetailView)
def get(self, request, *args, **kwargs):
if...:
return reverse('no_edit', kwargs={"pk": self.pk})
self is the DetailView object, which indeed has no primary key. If you want to access the object, you normally can use self.object, but since you have overwritten the get method itself, that will not work either.
You can fix it by calling self.get_object() here, like:
from django.shortcuts import redirect
class CheckView(DetailView):
# …
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if …:
return redirect('no_edit', pk=self.object.pk)
We can here make use of redirect(..) [Django-doc]. This will construct a HttpResponseRedirect with the result of a reverse call.
You're not returning a redirect, just an URL, try doing something like
return HttpResponseRedirect(reverse(...))

Django overriding detail view get method

This is my post detail view and it works perfectly.
class PostDetailView(DetailView):
model = Post
context_object_name = 'post'
template_name = 'posts/detail.html'
def get_queryset(self, *args, **kwargs):
request = self.request
pk = self.kwargs.get('pk')
queryset = Post.objects.filter(pk=pk)
return queryset
def get_context_data(self, **kwargs):
context = super(PostDetailView, self).get_context_data(**kwargs)
content['comments'] = Comment.objects.all()
return context
However, when I add get method to the view it does not work anymore.
def get(self, request, *args, **kwargs):
# how to return here so that it works exactly like before
After adding get method get_queryset and get_context_data do not gets called automatically and the context is empty in the template. So what would be the get method so that it works exactly like before?
EDIT
My target is to do something like this
if request.is_ajax():
html = render_to_string('comments/detail.html') # ajax reply with html data
return HttpResponse(html)
return render 'posts/detail.html'
So where do I put this code and still want to keep call all methods such as get_queryset and get_context_data to be called automatically?
The idea of views like a DetailView, ListView, etc. is that it implements the boilerplate logic for you. So it has defined a function def get(self, request, *args, **kwargs) that is used to render the logic. You can usually tweak a few things by specifying the model, queryset, etc. without reimplementing the entire view.
For a DetailView [Django-doc], the logic is implemented in the BaseDetailView you can inspect the source code [GitHub]:
class BaseDetailView(SingleObjectMixin, View):
"""A base view for displaying a single object."""
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
One general piece of advice I want to share:
Before overriding any attribute, one must have deep knowledge of what is the significance of that attribute (callable or not callable). This advice applies to any language or framework. Suppose when someone overrides the get in Django, all the methods that are being called from get will not be invoked unless one invokes that from overridden get. So you should see the source of get and observe that methods are called from that.

Django - Permissions and DeleteView, is this a good way to do it?

Hi so I have a working view. I wanted to know if this is the best or a good way of doing it. I want to have a delete view that deletes a Photo object but only if the logged in user is the on associated with the object.
Here is my views.py
class PhotoDelete(DeleteView):
model = Photo
template_name = 'otologue/photo_delete.html'
success_url = reverse_lazy('otologue:photos')
def get(self, request, *args, **kwargs):
object_instance = self.get_object() # Get the object
object_user = object_instance.photoextended.user # Get the user who owns the object
user = get_object_or_404(User, username=self.request.user) # Get the user in the view
if object_user != user: # See if the object_user is the same as the user
return HttpResponseForbidden('Permission Error')
else:
return render(request, self.template_name, {'object': object_instance})
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
If you need more info like the models and such, please request it.
You don't have to call get_object_or_404(), since self.request.user already contains an instance of User, so you can compare it directly.
You should call (and return the result) DeleteView's get() method instead of rendering the template yourself.
I would also use LoginRequiredMixin instead of a decorator, since your dispatch() now does nothing apart from calling super's method.
Be aware, that the actual deletion is done in the post() method. You should move your user check to the dispatch() method, otherwise it will be possible to bypass the check with a forged POST request.
To have better idea what DeleteView does in its methods, check it's source code (for example at ccbv.co.uk)

How to write separate views for GET and POST

First of all I want both views use exact same URL because I don't want to make my URLConf more complicated. I want separate views for GET and POST to make my code cleaner. The code is something like this:
def view2 (request):
# handle POST request, possibly a ajax one
return HTTPRESPONSE(json_data, mimetype="Application/JSON")
def view1 (request):
if method == POST:
view2(request)
# What should I return here???
else:
# handle GET
return render(request, template, context)
My question is about the # What should I return here??? line. If I don't put a return there, error occurs:
not returning http response
But I already return an HTTP response in view2. How can I make this work?
Another, probably a bit cleaner way would be using class-based views
from django.views.generic import TemplateView
class View1(TemplateView):
def get(self, request, *args, **kwargs):
"""handle get request here"""
def post(self, request, *args, **kwargs):
"""handle post request here"""
def head(self, request, *args, **kwargs):
"""handle head request here. Yes, you can handle any kind of requests, not just get and post"""
Of course you can add common methods, __init__ (which is useless unless you are sure what you are doing), apply login_required (see this SO question) and pretty much everything you can do with django views (e.g. apply middleware, permissions, etc.) and python classes (e.g. inheritance, metaclasses/decorators, etc.)
Also, there's a whole bunch of generic class based view coming with Django to address common situations like list page, details page, edit page, etc.
You need to return the results of view2:
def view1 (request):
if request.method == 'POST':
return view2(request)
else:
# handle GET
return render(request, template, context)

Get request.session from a class-based generic view

Is there a way to get request.session from inside a class-based view?
For instance, I have
from django.views.generic.edit import FormView
class CreateProfileView(FormView):
def form_valid(self, form):
# --> would like to save form contents to session here
return redirect(self.get_success_url())
The only thing I can think of would be override as_view by adding
def as_view(self, request, *args, **kwargs):
self.session = request.session
super(CreateProfileView, self).as_view(request, *args, **kwargs)
to the class. But that seems ugly. Is there another way?
You have access to self.request from anywhere within the class (and therefore self.request.session)
https://docs.djangoproject.com/en/dev/topics/class-based-views/generic-display/#dynamic-filtering
The key part to making this work is that when class-based views are called, various useful things are stored on self; as well as the request (self.request) this includes the positional (self.args) and name-based (self.kwargs) arguments captured according to the URLconf.