views:

113

answers:

4

I'm on the cusp of starting work on a new webapp. Part of this will give users pages that they can customise in a one to many relationship. These pages naturally need to have unique URLs.

Left to its own devices, Django would normally assign a standard AUTOINCREMENT ID to a model. While this works fantastically, it doesn't look great and it also makes pages very predictable (something that isn't desired in this case).

Rather than 1, 2, 3, 4 I would like set-length, randomly generated alphanumeric strings (eg h2esj4). 6 spots of a possible set of 36 characters should give me over two billion combinations which should be more than enough at this stage. Of course if I could expand this at a later time, that would be good too.

But there are two issues:

  1. Random strings occasionally spell out bad words or other offensive phrases. Is there a decent way to sidestep that? To be fair I could probably settle for a numeric string but it does have a hefty hit on the likelihood of clashes.

  2. How do I get Django (or the database) to do the heavy lifting on insert? I'd rather not insert and then work out the key (as that wouldn't be much of a key). I assume there are concurrency issues to be aware of too though if two new pages were generated at the same time and the second (against all odds) magically got the same key as the first before the first was committed.

I don't see this being a million miles different from how URL shorteners generate their IDs. If there's a decent Django implementation of one, I could piggyback off that.

+1  A: 

May be you need to look at Python UUID, it can generate random lengthy characters. But you can slice it and use the number of characters you want with little check to make sure it's unique even after slicing.

UUIDField snippet may help you if you don't want to take pain of generating UUID yourself.

Also have a look at this blog post

Srikanth Chundi
This doesn't really circumvent either of the two issues I highlight in the question. Granted `UUIDField` helps abstract some of the code away from my model but it's still outside the database (where I'd really like it) and still highly capable of spelling out rude words.
Oli
+3  A: 

There is built-in Django way to achieve what you want. Add a field to the model of "custom page" with primary_key=True and default= name of key generation function, like this:

class CustomPage(models.Model):
    ...
    mykey = models.CharField(max_length=6, primary_key=True, default=pkgen)
    ...

Now, for every model instance page, page.pk becomes an alias for page.mykey, which is being auto-assigned with the string returned by your function pkgen() at the moment of creation of that instance.
Fast&dirty implementation:

def pkgen():
    from base64 import b32encode
    from hashlib import sha1
    from random import random
    rude = ('lol',)
    bad_pk = True
    while bad_pk:
        pk = b32encode(sha1(str(random())).digest()).lower()[:6]
        bad_pk = False
        for rw in rude:
            if pk.find(rw) >= 0: bad_pk = True
    return pk

The probability of two pages getting identical primary keys is very low (assuming random() is random enough), and there are no concurrency issues. And, of couse, this method is easilly extensible by slicing more chars from encoded string.

atomizer
I don't understand the point of b32encode and sha1 in this concept. Wouldn't a simple random choice of a list of characters generate just as random a result, with a lot less overhead (and code)?
Oli
@Oli you can generate any string you want, the point is that setting a call back function to default is the way you would assign the string as the PK. Seems like right solution to me +1 Upvote
Rasiel
In a reusable setting, it can't do collision checking. There can't be more than once instance of a Model with the same slug. This is a flaw in the `default` argument not being able to take additional information (to pass the class to the generator).
Oli
+1  A: 

Here's what I ended up doing. I made an abstract model. My use-case for this is needing several models that generate their own, random slugs.

A slug looks like AA##AA so that's 52x52x10x10x52x52 = 731,161,600 combinations. Probably a thousand times more than I'll need and if that's ever an issue, I can add a letter for 52 times more combinations.

Use of the default argument wouldn't cut it as the abstract model needs to check for slug collisions on the child. Inheritance was the easiest, possibly only way of doing that.

from django.db import models
from django.contrib.auth.models import User

import string, random

class SluggedModel(models.Model):
    slug = models.SlugField(primary_key=True, unique=True, editable=False, blank=True)

    def save(self, *args, **kwargs):
        while not self.slug:
            ret = []
            ret.extend(random.sample(string.letters, 2))
            ret.extend(random.sample(string.digits, 2))
            ret.extend(random.sample(string.letters, 2))

            newslug = ''.join(ret)
            if self.objects.filter(pk=newslug).count():
                self.slug = newslug

        super(SluggedModel, self).save(*args, **kwargs)

    class Meta:
        abstract = True
Oli
Interesting. I've recently decided to move to a UUID generation approach for some pk's but I might consider this as well. Your fragment would actually work the same either way I think. Just replace the 4 lines you generate 'ret' with something like '''ret = uuid.uuid1()'''
Van Gale
A: 

Oli: If you're worried about spelling out rude words, you can always compare/search your UUIDField for them, using the django profanity filter, and skip any UUIDs that might be triggery.

Elf Sternberg