As expected, I was faced by the 'Create a new group'-page when I tried to view my pretty new group. When I swapped some URLs in de Groups app, everybody who tried to create a new group, was served the page of my beautiful group. Everything worked as expected, but still it was considered a bug. This was not the kind of functionality the customer was looking for. For the time being we just let it be (what are the chances the customer would create a group with the slug 'create'?), but the case kept whining in the back of my head.
Until today. I decided to tackle the problem. And of course, it was easier than I thought.
After entering some smart queries, Google told me that there is something called the Django test client. Django says: 'The test client is a Python class that acts as a dummy Web browser, allowing you to test your views and interact with your Django-powered application programmatically.' (source) In other words: The test client can be used to send a request to and get a response from your server. This could come in handy!
Long story short: I decided to create a SaveSlugField that uses the Django test client to check if a certain slug (or URL) already exists. If the slug already exists, it throws a standard validation error. Here's the code you've been craving for:
I think the code is pretty self explaining. This SaveSlugField checks if the URL www.domain.tld/<slug>/ is still available. Of course you can use subclassing (like I subclassed SlugField) to check for your custom slugs/URLs like www.domain.tld/group/<group_slug>/.
If there are any better, prettier, easier or faster way to solve this problem, do not hesitate to let me know in the comments!
EDIT:
Thanks @ polichism. I was afraid I might be kind of confusing ;-)
For the sake of clarity: The URL config in the Group app probably looked a little like this:
Long story short: I decided to create a SaveSlugField that uses the Django test client to check if a certain slug (or URL) already exists. If the slug already exists, it throws a standard validation error. Here's the code you've been craving for:
from django import forms from django.test.client import Client class SaveSlugField(forms.SlugField): def slug_url_exists(self, slug): """ This function uses the Django test Client to determine whether or not a given URL (or slug) is already being used in this website. """ c = Client() if not slug[0] == '/': slug = '/' + slug # Add a slash to create a valid URL response = c.get(slug) if response.status_code == 404: return False return True def clean(self, *args, **kwargs): data = super(SaveSlugField, self).clean(*args, **kwargs) if self.slug_url_exists(data): raise forms.ValidationError('This slug is not available') return data
I think the code is pretty self explaining. This SaveSlugField checks if the URL www.domain.tld/<slug>/ is still available. Of course you can use subclassing (like I subclassed SlugField) to check for your custom slugs/URLs like www.domain.tld/group/<group_slug>/.
If there are any better, prettier, easier or faster way to solve this problem, do not hesitate to let me know in the comments!
EDIT:
Thanks @ polichism. I was afraid I might be kind of confusing ;-)
For the sake of clarity: The URL config in the Group app probably looked a little like this:
url(r'^group/create/$', 'create', name = 'groups.create'), url(r'^group/(?P<slug>[-\w]+)/$', 'view', name = 'groups.view'),
Just asking:
ReplyDeleteWas:
slug = models.SlugField(unique=True)
Not enough?
No.
ReplyDeleteUnique makes sure you don't enter a slug like 'hello-world' twice, but if you already defined (in the URL config) a page like '/group/create/' it does not stop you from creating a group with the slug 'create'. Even if a group with that slug will conflict (through the URL config) with the Create Group page.
Fair point then :) nice piece of work, and thanks of sharing knowledge
ReplyDeleteMade it a little clearer, hopefully ;-)
ReplyDeleteThx for the feedback and compliments!
Read the article once more and notice what the real problem is...
ReplyDeleteNice Post!
ReplyDeleteRegarding the URL to resolve it might be better to use reverse() (See: http://docs.djangoproject.com/en/dev/topics/http/urls/#topics-http-reversing-url-namespaces) instead of '/'+slug ? The SaveSlugField should get the name of the view upon instantiating..
And maybe it's useful to use namespaces:
ReplyDeleteroot url:
(r'^group/', include('group.urls', namespace="group"))
group url:
url(R'^create/$', 'group.views.create', name="create")
So you can use reverse in your views and your templates.
template:
{% load url %} {% url "group:create" %}
It would be better to use the standard resolve function (django.core.urlresolvers.resolve) instead of the test client I think. The test client is intended for testing.
ReplyDeleteSee http://docs.djangoproject.com/en/dev/topics/http/urls/#django.core.urlresolvers.resolve
Also, using the url dispatcher directly is faster than the exception throwing test client.
The real problem seems to be that you're using the same URL pattern for two different purposes. It may not be the same URL pattern in your URLConf, but if there's even the possibility of a conflict, there is something wrong.
ReplyDeleteI think a much better solution would be to modify your URLs. There are two glaring options.
/groups/create/
/groups/view/<slug>/
or
/groups/create/
/groups/<group_id>/<optional_slug>
@ionelmc: Thx, I'll dive into it and try to fiddle it out when I got the time. :-)
ReplyDelete@Josh.Smeaton: True, but what if, for example, you've got a CMS? It's not really pretty to let users create their own URLS (users want that sometimes), but force them to use '/view_pages/'. Besides, it seems that search engines don't like that either ;-) To fix that problem, I was looking for a pretty solution and this was what I came up with.
I would follow the implementation done by http://www.elfsternberg.com/2009/06/26/dynamic-names-as-first-level-url-path-objects-in-django/
ReplyDeleteYou could also try my project django-aislug, https://github.com/aino/django-aislug. It will automatically compute a unique slug for a model. You can provide invalid slugs, either from a list or a callback function. Another thing to consider is that updating slugs are mostly not such a good idea, django-aislug provides a keyword argument 'update' to control this behaviour too.
ReplyDeleteI don't think that this solution solves completely the problem. Imagine that, after using this implementation, someone creates a group named "coolest" (just an example), and months later you want to add a new function to your application to show the coolest groups. I think that you could not use a url like '/groups/coolest/', because you would have the same conflict, but in a reverse direction.
ReplyDeleteGood point strider. I guess the most fool-proof way to prevent URL collision is to make a seperate URL for displaying objects with a slug. I'll give it some more thoughts though.
Delete