17

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.

Ludo
  • 2,739
  • 2
  • 28
  • 42

2 Answers2

16

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.

Rolo
  • 6,070
  • 1
  • 21
  • 12
  • 1
    You can just call the parents get method in the else case here. It'd be much cleaner. It's totally worth it as class based generic views are there to be extended for custom functionality. – vim Jun 23 '11 at 16:45
  • 1
    What's good about your approach is that we then don't need to worry if the implementation of BaseDetailView.get changes, but the downside is that we'd need to do the object retrieval twice for every request, which for me isn't worth the potential performance/scalability hit. – Rolo Jun 24 '11 at 11:52
  • For avoiding the double hit to `get_object` you may override it in your own class or mixin and preface with a check `if hasattr(self, 'object', None)`; if the check succeeds, return `self.object` otherwise call parent’s `get_object`. Exactly as @Raumkraut implemented (https://stackoverflow.com/a/12858110). – interDist May 26 '17 at 12:05
10

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.

Romløk
  • 181
  • 2
  • 4