I manage a physical locker with Django (DRF). Users fill out a form, authenticate via link sent to their e-mail, authorize via a pin displayed on the locker.
My view should handle three cases:
If user authenticates and authorizes successfully, pin displayed on the locker is replaced with a generic message and the locker opens. (Already implemented)
If the user fails to authorize within 3 minutes, locker pin is replaced with a generic message.
If a new authorization request is made by user Foo, while authorization for user Bar is still incomplete, stack the request in a queue and wait for case 1. or case 2. to complete.
How can I:
View as is, in case it is useful:
if request.method == 'POST':
form = ConfirmationForm(request.POST)
if form.is_valid():
if pin == form.cleaned_data['pin']:
open_bay(jwt_token=jwt[1], pin=pin)
display_generic_message(jwt_token=jwt[1])
lock_bay(jwt_token=jwt[1], pin=pin)
return render(request, 'static/pages/request-success.html')
else:
pass
else:
form = ConfirmationForm()
return render(request, 'static/pages/confirmation.html', {'form': form})
in your model which u save the authorization u need to add a field of date_created or date_requested and must be a datetime field . then on each request you can check if 3 minutes has passed from date_requested which you saved . you also need an is_authorized field to check if your user is authorized or not. we assume that you are getting the user from email you sent .
user = get_object_or_404(User,email=kwargs['email'])
if user.date_request + timedelta(minutes=3) > datetime.datetime.now():
"do your authorzing stuff ..."
else:
return HttpResponse("you need to wait 3 minutes to request again")
You would need to store when the last authorization started, clear that timestamp if the user puts in the pin.
# models.py
class LockerUserQueue(models.Model):
user = models.ForeignKey(get_user_model())
locker = models.ForeignKey("yourapp.Locker")
created_at = models.DateTimeField(index=True, auto_now_add=True)
class Locker(models.Model):
last_authorization = models.DateTimeField(null=True, blank=True)
user_queue = models.ManyToManyField(through=LockerUserQueue)
class BadPin(Exception):
pass
def enqueue_user(self, user):
self.user_queue.add(user)
def process_authorization(self, user):
# do authorization for user
def process_pin(self, user, pin):
self.last_authorization = None
if validate_pin(user, pin):
# pin OK logic
else:
raise self.BadPin
# views.py
def authorize_user(request, locker_id):
locker = get_object_or_404(pk=locker_id)
locker.enqueue_user(request.user)
locker.save()
return render(request, "authorization_started.html")
def open_bay_with_pin(request, locker_id):
locker = get_object_or_404(pk=locker_id)
pin = get_pin_from_request(request)
try:
# No matter if the pin is correct, the locker.last_authorization is cleared
locker.process_pin(user, pin)
except locker.BadPin:
return render(request, "bad_pin.html")
finally:
locker.save()
return render(request, "good_pin.html")
# management/commands/process_queue.py
# you would run this by
# $ python manage.py process_queue
class Command(BaseCommand):
@transaction.atomic
def process_queue(self):
# you probably want to put the 3 min delay in your settings.py
# so you don't end up with a magic value here
for locker in LockerUserQueue.objects.filter(
Q(locker__last_authorization__isnull=True)|Q(locker__last_authorization__gt=now() - timedelta(minutes=3))
).order_by("created_at").values_list("locker", flat=True).distinct():
locker.process_authorization()
def handle(self):
while True:
self.process_queue()
# you don't want to keep querying the DB when there's nothing in the queue
sleep(5)
The code above shows how you would do it using the database as the queue, in a low volume use case this should be just fine. If the volume is high, I would store the locker last_authorization in faster storage, like redis, the queue can be maintained in redis as well. But the logic behind would be the same.