Quantcast
Channel: Peter of the Norse Thinks He’s Smarter than You
Viewing all articles
Browse latest Browse all 54

Sending an error when it’s not quite unique enough

$
0
0

Back in 2005 I mentioned I use to_ascii to make sure that there aren’t accented and un-accented versions of artists. Like Hüsker Dü. That one always gets people.

The problem is, there’s no way to easily use PostgreSQL’s built-in to_ascii for searching and unique restraints. We’ll have to make our own. First is to take slugifyhifi and change it for our use. There’s a lot wrong with the one floating on the internet. It returns a binary string instead of unicode despite Django only working with unicode. It also maps things based on the language of origin. But most of the bands – and almost all of the DJs – don’t work that way. Jón Þór Birgisson should be Jon Thor, but most searches are for Jon Por. “Яussian” is common, despite the fact that Я is a vowel. So I changed things so that each letter is mapped to what it looks like, instead of how it’s pronounced. I.e. ß→B instead of ss. I call it ugam for ugly american.

So where do we store this information? A seperate field called search. So I over write save for artists, albums and songs.

    def save(self, *args, **kwargs):
        self.search = extras.search_sort(self.artist)
        return super(Artist, self).save(*args, **kwargs)

The other problem is I don’t want to have both Husker Du and Hüsker Dü in the database, so I mark search as unique in the Artist class. This raises another problem. The database will raise a uniqueness error, but doesn’t pass it to the form, so all that the user sees is a 301 error. We need to raise a ValidationError. And set it for the artist name when the search field isn’t unique. Wow! Time to trace execution.

Looking though, we see that the model’s validate_unique method is called. Since there isn’t an underscore in front of it, it should be safe to extend.

def validate_unique(self, exclude=None):
        super(Artist, self).validate_unique(exclude)
        
        if "artist" in exclude:
            return
        
        qs = Artist.objects.filter(search=extras.search_sort(self.artist))
        if self.pk is not None:
            qs = qs.exclude(pk=self.pk)
        if qs.exists():
            raise ValidationError({'artist':(u'An artist named ‘%s’ already exists' % qs[0].artist, )})

The absolute right thing to do is to catch an ValidationError and if there isn’t an existing problem with artist then we run our test too. But that’s more work than we need. Mostly because the only other unique field is artist.

If artist is excluded, don’t look. If we have a pk, we don’t want to match ourselves. (Again, the right thing to do is to watch for a “Save as new”.) The nice thing about this query set is it returns the name of the other artist that we’re matching, since it might be something that the DJ doesn’t know is in the database.

This took a long time to figure out, because it’s not something that the Django people have documented. I first looked at adding it to the form, perhaps as part of a hidden widget or something. Since 1.2, the uniqueness validation is handled by the model. Other validators are copied from the model to the model form. It’s the right thing to do, I just wasn’t expecting it. Earlier versions had the ModelForm doing it all by itself.


Viewing all articles
Browse latest Browse all 54

Trending Articles