How can I make Stackoverflow-style URLs in Django with a working reverse, that allow the slug to change?

531 Views Asked by At

How can I make an URL scheme that works just like Stackoverflow?

This isn't the same question as this one, though it is similar. The difference is that I want an URL scheme that implements all of Stackoverflow's cleverness, and allows reverseing to generate fully slugged URLs.

Specifically, the Stackoverflow behaviour to mimic:

  1. Lookup by id only, with slug only for SEO/readability purposes
  2. Forwarding to correct slug when the incorrect slug, or no slug is given. e.g. if I have an object 123 with a name My Object Name:

    /123/ redirects to /123/my-object-name/
    /123/something/ redirects to /123/my-object-name/
    
  3. If I change the object name to My New Object Name then the redirect target slug changes accordingly (this is Stackoverflow's behaviour, if you edit your question's title), e.g.:

    /123/my-object-name/ redirects to /123/my-new-object-name/
    
  4. Reverse working so that {% url 'my_view' 123 %} returns /123/my-object-name/, and after editing the object name, returns /123/my-new-object-name/

I've hacked something whereby I use a models.py:

class MyModel(models.Model):
    name = models.CharField(max_length=70)

    def my_slugged_url(self):
        slug = slugify(self.name)
        if slug:
            return reverse('my_view', args=[self.id]) + slug + "/"
        return reverse('my_view', args=[self.id])

...and a urls.py pattern:

url(r'^(\d+)/\S+/$', 'my_view')
url(r'^(\d+)/$', 'my_view'),

...and a views.py:

def my_view(request, id):
    obj = get_object_or_404(MyModel, pk=id)
    if request.path != obj.my_slugged_url():
        return redirect(obj.my_slugged_url())

...but this feels wrong, and means when I do a reverse or {% url 'my_view' 123 %} it returns a URL like /123/ that then has to redirect to /123/my-object-name.

How can I make this work just like Stackoverflow?

2

There are 2 best solutions below

5
On
# views
def detail(request, object_id, slug):
    obj = get_object_or_404(MyModel, pk=object_id)
    if obj.slug != slug:
        canonical = obj.get_absolute_url()
        return redirect(canonical)

    context = {"obj":obj}
    return render(request, "myapp/detail.html", context)


# urls
from myapp.views import detail
urlpatterns = ('',
    #...
    url(r'^(?P<object_id>\d+)/(<?P<slug>\S+)/$', detail, name="detail")
    url(r'^(\d+)/$', lambda request, pk: detail(request, pk, None), name="redirect-to-detail"),
    # ...
    )

# models
class MyModel(models.Model):
    def get_absolute_url(self):
        return reverse(
          "detail", 
          kwargs=dict(object_id=self.id, slug=self.slug)
          )

    @property
    def slug(self):
        return slugify(self.title)
0
On

Given your recurring comment – "... how would the pattern know which slug to return", it appears that you are having trouble understanding how it works. I'll try to break down the process for you.

First of all, you will write two url patters pointing to one view. Remember to give both patterns different name.

# urls.py
...
url(r'^(?P<object_id>\d+)/$', 'my_view', name='my-view-no-slug'),
url(r'^(?P<object_id>\d+)/(?P<slug>\S+)/$', 'my_view', name='my-view-slug'),
...

Now, this is where it gets interesting:

  1. Whenever a request is made to /123/, it will match the first url pattern.
  2. When a request is made to /123/my-object-name/, it will match the second pattern.
  3. When a request is made with a wrong slug, like - /123/some-wrong-slug/, it will also match the second pattern. Don't worry, you'll check for wrong slug in your view.

But all three requests will be handled by one view.

Second, define a property called slug in your model. You will use this to generate and access slugs of objects.

# models.py

class MyModel(...):
    ...

    @property
    def slug(self):
        return slugify(self.name)

    def get_absolute_url(self):
        return reverse('my-view-slug', args=[self.id, self.slug])

And finally, the view which will handle the requests should look something like this:

# views.py

def my_view(request, object_id, slug=None):
    # first get the object
    my_object = get_object_or_404(MyModel, id=object_id)

    # Now we will check if the slug in url is same 
    # as my_object's slug or not
    if slug != my_object.slug:
        # either slug is wrong or None
        return redirect(my_object.get_absolute_url())

    # this is processed if slugs match
    # so do whatever you want
    return render(request, 'my-template.html', {'my_object': my_object})

I hope this makes it clear how to implement a StackOverflow-like url behaviour.