Hassan Baig
Hassan Baig

Reputation: 15824

Tricky coding logic for Django web app

I have some peculiar requirements, and I'm trying to figure out the best approach to code it. Best being efficiency foremost and maintainability second-most. The requirement goes something like this (bear with me):

I have a feature on my Django website that's meant to serve both registered and unregistered users. This feature is going to be gated behind a pin code for each user. I require these pins to be randomly generated (and not serially).

The pin code will need to be memorable - which means pin len becomes a factor. I'm going for a 4 digit pin hence. And numbers only, since alphanumeric is less memorable than purely digits (according to usability tests I've conducted - I'm sticking to these results).

I can't have collisions among pin codes since they'll be used as identifiers too - so all of them must be unique. My range will lie between 0000 to 9999. Only 10K unique combinations will be possible. This is restrictive, but memorability of the pin code is a higher priority than the size of the pool of possible pin codes. So I'll sacrifice.

Lastly, pin codes for registered users will be permanently assigned. Unregistered users, on the other hand, will be assigned pins that remain booked for no more than 24 hrs, after which they expire - and hence enter the pool of unused pins once more.

Imagine my data model for the above (in models.py) is like so:

class Inbox(models.Model):
    pin_code = models.CharField(default='0')
    owner = models.ForeignKey(User)
    creation_time = models.DateTimeField(auto_now_add=True)

In views.py, I'll need a way to allot an available pin_code to each Inbox object I create. What's the most efficient logic? Here's how I'm thinking I can approach it:

def expire_pin(time_difference=None):

    #admin user (with id 1) is assumed as the 'unregistered user'
    Inbox.objects.filter(creation_time__lte=time_difference,owner_id=1).update(pin_code='0')

def get_pin():

    parent_list = ['{:04d}'.format(i) for i in range(10000)]
    day_ago = timezone.now() - timedelta(hours=24)
    expire_pin(day_ago)
    to_exclude = Inbox.objects.filter(~Q(pin_code='0')).values_list('pin_code',flat=True)
    new_list = [item for item in parent_list if item not in to_exclude]
    return random.choice(new_list)

Optional: you don't have to read it, but here is what I'm using the pin_code for. Every user - registered and unregistered - is allotted an inbox that is accessible at example.com/pin/XXXX (XXXX being the pin_code). Users can share this inbox address with their friends over social media. Friends can then log into it via their mobile number, viewing content the former user left particularly for the eyes of the said friend. Catch the drift?

I need this feature for both registered and unregistered users - thus the need to allot a pin_code even to unknown users. But I want to be able to recycle unregistered user codes, so that I don't run out of the 10K possibilities too fast. I have a moderately big user base on this website.

Ultimately though, I will run out of 10K combinations. I'm thinking I'll write code that seamlessly shifts over to 5-digit pins thereafter. But even in that scenario, if any 4-digit pins expire and become available, they'll get first priority during allotment. If you can help me with that part as well, great!

Upvotes: 1

Views: 203

Answers (4)

Artur Barseghyan
Artur Barseghyan

Reputation: 14172

All you need is RandomCharField of the django-extensions package. See the django-extensions docs.

RandomCharField(length=4, include_alpha=False)
# 7097

In your code:

from django_extensions.db.fields import RandomCharField

class Inbox(models.Model):
    pin_code = RandomCharField(length=4, unique=True, include_alpha=False)
    owner = models.ForeignKey(User)
    creation_time = models.DateTimeField(auto_now_add=True)

Upvotes: 1

Tanner
Tanner

Reputation: 630

What if you instead had

class PinCode(models.Model):
    code = models.CharField(max_length='4', primary_key=True, validators=[lambda x: len(x) == 4])
    current_owner = models.ForeignKey(User, blank=True, null=True)
    created = models.DateTimeField(auto_now_add=True)

then to get a pin you simply simply query PinCodes for the next one where it is unowned or past due.

Upvotes: 0

warath-coder
warath-coder

Reputation: 2122

For pin code assignment; I'm fairly sure your code is doing something similar:

First I would keep a list of all code possibilities (might as well create the list now), so would be a simple table: unique-4-digit-code:assigned-boolean

then I would create a function:

sudo code:

def new_pin(db_list):
    # db_list is passed in array of db values where assigned-boolean = false
    random number between 1 and 9998 # i wouldn't use 0000 and 9999, may even want to remove all 1111, 2222, but will further reduce pool size
    if rand in db_list:
        db.assigned-boolean = true for rand
        return rand
    else:
        return new_pin(db_list)

Issues with above; you can have race conditions, so would have to lock the database table for db_list and other calls would have to wait until db_list is available so that you don't assign a pin twice by accident (unlikely with using rand, but could happen on a busy enough site with the small pool).

Another option would be to add more complexity to your url, for example example.com/USER_NAME/XXXX

and let the user pick the XXXX and make sure they have a unique name. For those not registered, you can assign a random word.

If that is too complex; I would suggest allowing codes from 3 to 6 in length so that example.com/pin/XXX example.con/pin/XXXX example.com/pin/XXXXX, etc

all work if pin assigned. This would drastically increase your pool size.

Upvotes: 0

Wolph
Wolph

Reputation: 80031

Quite a story but I'm not a 100% certain what is your question. How to generate random numbers?

import random
print('%04d' % random.randint(0, 10000))

Put that in a while loop to make sure it doesn't exist in the database yet. Alternatively, pregenerate a list of 10000 pin numbers in your database with a used boolean to get a slightly more efficient solution.


I have to ask though, is remembering really a requirement? If it is, isn't a custom string a better idea? Random numbers are not that easy to remember, people are much better at remembering something they thought of themselves.

Upvotes: 0

Related Questions