from itertools import chain from random import sample from django.db import IntegrityError from django.db.models.signals import post_save, pre_save, pre_delete, post_init from django.dispatch import receiver from pinaxcon.raffle.models import DrawnTicket, Raffle, Draw, Prize # Much of the following could be handled by directly overriding the # relevant model methods. However, since `.objects.delete()` bypasses # a model's delete() method but not its pre_ and post_delete signals, # using signals gives us slightly better coverage of edge cases. # # In order to avoid mixing the two approaches we make extensive use of # signals. @receiver(post_save, sender=Draw) def draw_raffle_tickets(sender, instance, created, **kwargs): """ Draws tickets once a :model:`pinaxcon_raffle.Draw` instance has been created and prizes are still available. """ if not created: return raffle = instance.raffle prizes = raffle.prizes.filter(winning_ticket__isnull=True) tickets = list(chain(*(ticket[1] for ticket in raffle.get_tickets()))) if not tickets: return drawn_tickets = sample(tickets, len(prizes)) for prize, ticket in zip(prizes, drawn_tickets): item_id = int(ticket.split('-')[0]) drawn_ticket = DrawnTicket.objects.create( draw=instance, prize=prize, ticket=ticket, lineitem_id=item_id, ) prize.winning_ticket = drawn_ticket prize.save(update_fields=('winning_ticket',)) @receiver(post_init, sender=Prize) def set_prize_lock(sender, instance, **kwargs): """Locks :model:`pinaxcon_raffle.Prize` if a winner exists.""" instance._locked = instance.winning_ticket is not None @receiver(pre_save, sender=Prize) def enforce_prize_lock(sender, instance, **kwargs): """Denies updates to :model:`pinaxcon_raffle.Prize` if lock is in place.""" if instance.locked: raise IntegrityError("Updating a locked prize is not allowed.") @receiver(pre_delete, sender=Prize) def prevent_locked_prize_deletion(sender, instance, **kwargs): """Denies deletion of :model:`pinaxcon_raffle.Prize` if lock is in place.""" if instance.locked: raise IntegrityError("Deleting a locked prize is not allowed.") @receiver(pre_delete, sender=DrawnTicket) def prevent_drawn_ticket_deletion(sender, instance, **kwargs): """Protects :model:`pinaxcon_raffle.DrawnTicket` from deletion if lock is in place.""" if instance.locked: raise IntegrityError("Deleting a drawn ticket is not allowed.") @receiver(pre_save, sender=DrawnTicket) def prevent_drawn_ticket_update(sender, instance, **kwargs): """Protects :model:`pinaxcon_raffle.DrawnTicket` from updates.""" if getattr(instance, 'pk', None): raise IntegrityError("Updating a drawn ticket is not allowed.")