Add prototype for web-based raffle
This commit is contained in:
		
							parent
							
								
									68ea59f4d7
								
							
						
					
					
						commit
						d1ff8d7253
					
				
					 15 changed files with 548 additions and 1 deletions
				
			
		
							
								
								
									
										1
									
								
								pinaxcon/raffle/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								pinaxcon/raffle/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | default_app_config = 'pinaxcon.raffle.apps.RaffleConfig' | ||||||
							
								
								
									
										50
									
								
								pinaxcon/raffle/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								pinaxcon/raffle/admin.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | ||||||
|  | from django.contrib import admin | ||||||
|  | 
 | ||||||
|  | from pinaxcon.raffle import models | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ReadOnlyMixin: | ||||||
|  |     actions = None | ||||||
|  |     list_display_links = None | ||||||
|  | 
 | ||||||
|  |     def has_add_permission(self, request): | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def has_delete_permission(self, request, obj=None): | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def save_model(self, request, obj, form, change): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DrawAdmin(ReadOnlyMixin, admin.ModelAdmin): | ||||||
|  |     list_display = ('raffle', 'drawn_time', 'drawn_by') | ||||||
|  |     readonly_fields = ('raffle', 'drawn_time', 'drawn_by') | ||||||
|  |     list_filter = ('raffle',) | ||||||
|  | 
 | ||||||
|  |     ordering = ('raffle', '-drawn_time') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DrawnTicketAdmin(ReadOnlyMixin, admin.ModelAdmin): | ||||||
|  |     list_display = ('draw', 'ticket') | ||||||
|  |     readonly_fields = ('draw', 'ticket', 'lineitem', 'prize') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AuditAdmin(ReadOnlyMixin, admin.ModelAdmin): | ||||||
|  |     list_display = ('timestamp', 'raffle', 'prize', 'reason', 'user',) | ||||||
|  |     list_filter = ('prize__raffle',) | ||||||
|  |     readonly_fields = ('reason', 'prize', 'user') | ||||||
|  | 
 | ||||||
|  |     def raffle(self, instance): | ||||||
|  |         return instance.prize.raffle | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PrizeAdmin(admin.ModelAdmin): | ||||||
|  |     readonly_fields = ('winning_ticket',) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | admin.site.register(models.Raffle) | ||||||
|  | admin.site.register(models.Prize, PrizeAdmin) | ||||||
|  | admin.site.register(models.Draw, DrawAdmin) | ||||||
|  | admin.site.register(models.DrawnTicket, DrawnTicketAdmin) | ||||||
|  | admin.site.register(models.PrizeAudit, AuditAdmin) | ||||||
							
								
								
									
										17
									
								
								pinaxcon/raffle/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								pinaxcon/raffle/apps.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | from django.apps import AppConfig | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class RaffleConfig(AppConfig): | ||||||
|  |     name = "pinaxcon.raffle" | ||||||
|  |     label = "pinaxcon_raffle" | ||||||
|  |     verbose_name = "Pinaxcon Raffle" | ||||||
|  |     admin_group_name = "Raffle admins" | ||||||
|  | 
 | ||||||
|  |     def ready(self): | ||||||
|  |         import pinaxcon.raffle.signals | ||||||
|  |      | ||||||
|  |     def get_admin_group(self): | ||||||
|  |         from django.contrib.auth.models import Group | ||||||
|  | 
 | ||||||
|  |         group, created = Group.objects.get_or_create(name=self.admin_group_name) | ||||||
|  |         return group | ||||||
							
								
								
									
										93
									
								
								pinaxcon/raffle/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								pinaxcon/raffle/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Generated by Django 1.11.15 on 2019-01-01 03:32 | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | 
 | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import migrations, models | ||||||
|  | import django.db.models.deletion | ||||||
|  | import pinaxcon.raffle.mixins | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     initial = True | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ('registrasion', '0008_auto_20170930_1843'), | ||||||
|  |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Draw', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('drawn_time', models.DateTimeField(auto_now_add=True)), | ||||||
|  |                 ('drawn_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='DrawnTicket', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('ticket', models.CharField(max_length=255)), | ||||||
|  |                 ('draw', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinaxcon_raffle.Draw')), | ||||||
|  |                 ('lineitem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='registrasion.LineItem')), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Prize', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('description', models.CharField(max_length=255)), | ||||||
|  |                 ('order', models.PositiveIntegerField()), | ||||||
|  |             ], | ||||||
|  |             bases=(pinaxcon.raffle.mixins.PrizeMixin, models.Model), | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='PrizeAudit', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('reason', models.CharField(max_length=255)), | ||||||
|  |                 ('timestamp', models.DateTimeField(auto_now_add=True)), | ||||||
|  |                 ('prize', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='audit_events', to='pinaxcon_raffle.Prize')), | ||||||
|  |                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'ordering': ('-timestamp',), | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Raffle', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('description', models.CharField(max_length=255)), | ||||||
|  |                 ('products', models.ManyToManyField(to='registrasion.Product')), | ||||||
|  |             ], | ||||||
|  |             bases=(pinaxcon.raffle.mixins.RaffleMixin, models.Model), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='prize', | ||||||
|  |             name='raffle', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prizes', to='pinaxcon_raffle.Raffle'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='prize', | ||||||
|  |             name='winning_ticket', | ||||||
|  |             field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='pinaxcon_raffle.DrawnTicket'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='drawnticket', | ||||||
|  |             name='prize', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinaxcon_raffle.Prize'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='draw', | ||||||
|  |             name='raffle', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='draws', to='pinaxcon_raffle.Raffle'), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterUniqueTogether( | ||||||
|  |             name='prize', | ||||||
|  |             unique_together=set([('raffle', 'order')]), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										32
									
								
								pinaxcon/raffle/migrations/0002_auto_20190102_1205.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								pinaxcon/raffle/migrations/0002_auto_20190102_1205.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Generated by Django 1.11.14 on 2019-01-02 01:05 | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | 
 | ||||||
|  | from django.db import migrations | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_admin_group_name(apps): | ||||||
|  |     from pinaxcon.raffle.apps import RaffleConfig | ||||||
|  |     return RaffleConfig.admin_group_name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def create_auth_group(apps, schema_editor): | ||||||
|  |     Group = apps.get_model("auth", "Group") | ||||||
|  |     Group.objects.get_or_create(name=get_admin_group_name(apps)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def delete_auth_group(apps, schema_editor): | ||||||
|  |     Group = apps.get_model("auth", "Group") | ||||||
|  |     Group.objects.filter(name=get_admin_group_name(apps)).delete() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ('pinaxcon_raffle', '0001_initial'), | ||||||
|  |         ('auth', '0001_initial') | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.RunPython(create_auth_group, delete_auth_group), | ||||||
|  |     ] | ||||||
							
								
								
									
										0
									
								
								pinaxcon/raffle/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								pinaxcon/raffle/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										56
									
								
								pinaxcon/raffle/mixins.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								pinaxcon/raffle/mixins.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | ||||||
|  | from functools import partial | ||||||
|  | 
 | ||||||
|  | from registrasion.models.commerce import Invoice, LineItem | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def generate_ticket(prefix, length, num): | ||||||
|  |     return "%d-%0*d" % (prefix, length, num) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def create_ticket_numbers(item): | ||||||
|  |     quantity = item['quantity'] | ||||||
|  |     length = len(str(quantity)) | ||||||
|  |     ticket_func = partial(generate_ticket, item['id'], length) | ||||||
|  |     return map(ticket_func, range(1, quantity+1)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class RaffleMixin: | ||||||
|  |     @property | ||||||
|  |     def is_open(self): | ||||||
|  |         prizes = self.prizes.all() | ||||||
|  |         return len(prizes) and not all(p.locked for p in prizes) | ||||||
|  | 
 | ||||||
|  |     def draw(self, user): | ||||||
|  |         self.draws.create(drawn_by=user) | ||||||
|  | 
 | ||||||
|  |     def get_tickets(self, user=None): | ||||||
|  |         filters = { | ||||||
|  |             'invoice__status': Invoice.STATUS_PAID, | ||||||
|  |             'product__in': self.products.all() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if user is not None: | ||||||
|  |             filters['invoice__user'] = user | ||||||
|  | 
 | ||||||
|  |         for item in LineItem.objects.filter(**filters).values('id', 'quantity'): | ||||||
|  |             yield (item['id'], list(create_ticket_numbers(item))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PrizeMixin: | ||||||
|  |     @property | ||||||
|  |     def locked(self): | ||||||
|  |         return self._locked | ||||||
|  | 
 | ||||||
|  |     def unlock(self, user): | ||||||
|  |         self.audit_events.create(user=user, reason="Unlocked") | ||||||
|  |         self._locked = False | ||||||
|  | 
 | ||||||
|  |     def remove_winner(self, user): | ||||||
|  |         reason = "Removed winning ticket: {}".format(self.winning_ticket.id) | ||||||
|  |         self.audit_events.create(user=user, reason=reason) | ||||||
|  |         self.winning_ticket = None | ||||||
|  |         self.save(update_fields=('winning_ticket',)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
							
								
								
									
										85
									
								
								pinaxcon/raffle/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								pinaxcon/raffle/models.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,85 @@ | ||||||
|  | from django.db import models | ||||||
|  | 
 | ||||||
|  | from pinaxcon.raffle.mixins import PrizeMixin, RaffleMixin | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Raffle(RaffleMixin, models.Model): | ||||||
|  |     """ | ||||||
|  |     Stores a single Raffle object, related to one or many | ||||||
|  |     :model:`pinaxcon_registrasion.Product`, which  is usually a raffle ticket, | ||||||
|  |     but can be set to tickets or other products for door prizes. | ||||||
|  |     """ | ||||||
|  |     description = models.CharField(max_length=255) | ||||||
|  |     products = models.ManyToManyField('registrasion.Product') | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.description | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Prize(PrizeMixin, models.Model): | ||||||
|  |     """ | ||||||
|  |     Stores a Prize for a given :model:`pinaxcon_raffle.Raffle`. | ||||||
|  | 
 | ||||||
|  |     Once `winning_ticket` has been set to a :model:`pinaxcon_raffle.DrawnTicket` | ||||||
|  |     object, no further changes are permitted unless the object is explicitely | ||||||
|  |     unlocked. | ||||||
|  |     """ | ||||||
|  |     description = models.CharField(max_length=255) | ||||||
|  |     raffle = models.ForeignKey('pinaxcon_raffle.Raffle', related_name='prizes') | ||||||
|  |     order = models.PositiveIntegerField() | ||||||
|  |     winning_ticket = models.OneToOneField( | ||||||
|  |         'pinaxcon_raffle.DrawnTicket', null=True, | ||||||
|  |         blank=True, related_name='+', on_delete=models.PROTECT | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         unique_together = ('raffle', 'order') | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return f"{self.order}. Prize: {self.description}" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PrizeAudit(models.Model): | ||||||
|  |     """ | ||||||
|  |     Stores an audit event for changes to a particular :model:`pinaxcon_raffle.Prize`. | ||||||
|  |     """ | ||||||
|  |     reason = models.CharField(max_length=255) | ||||||
|  |     prize = models.ForeignKey('pinaxcon_raffle.Prize', related_name='audit_events') | ||||||
|  | 
 | ||||||
|  |     user = models.ForeignKey('auth.User') | ||||||
|  |     timestamp = models.DateTimeField(auto_now_add=True) | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         ordering = ('-timestamp',) | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.reason | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Draw(models.Model): | ||||||
|  |     """ | ||||||
|  |     Stores a draw for a given :model:`pinaxcon_raffle.Raffle`, along with audit fields | ||||||
|  |     for the creating :model:`auth.User` and the creation timestamp. | ||||||
|  |     """ | ||||||
|  |     raffle = models.ForeignKey('pinaxcon_raffle.Raffle', related_name='draws') | ||||||
|  |     drawn_by = models.ForeignKey('auth.User') | ||||||
|  |     drawn_time = models.DateTimeField(auto_now_add=True) | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return f"{self.raffle}: {self.drawn_time}" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DrawnTicket(models.Model): | ||||||
|  |     """ | ||||||
|  |     Stores the result of a ticket draw, along with the corresponding | ||||||
|  |     :model:`pinaxcon_raffle.Draw`, :model:`pinaxcon_raffle.Prize` and the | ||||||
|  |     :model:`registrasion.commerce.LineItem` from which it was generated. | ||||||
|  |     """ | ||||||
|  |     ticket = models.CharField(max_length=255) | ||||||
|  | 
 | ||||||
|  |     draw = models.ForeignKey('pinaxcon_raffle.Draw') | ||||||
|  |     prize = models.ForeignKey('pinaxcon_raffle.Prize') | ||||||
|  |     lineitem = models.ForeignKey('registrasion.LineItem') | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return f"{self.ticket}: {self.draw.raffle}" | ||||||
							
								
								
									
										80
									
								
								pinaxcon/raffle/signals.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								pinaxcon/raffle/signals.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,80 @@ | ||||||
|  | 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.""" | ||||||
|  |     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.") | ||||||
							
								
								
									
										10
									
								
								pinaxcon/raffle/urls.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								pinaxcon/raffle/urls.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | from django.conf.urls import url | ||||||
|  | from pinaxcon.raffle import views | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | urlpatterns = [ | ||||||
|  |     url(r'^tickets/', views.raffle_view), | ||||||
|  |     url(r'^draw/(?P<raffle_id>[0-9]+)/$', views.draw_raffle_ticket, name="raffle-draw"), | ||||||
|  |     url(r'^draw/redraw/([0-9]+)/$', views.raffle_redraw, name="raffle-redraw"), | ||||||
|  |     url(r'^draw/', views.draw_raffle_ticket, name="raffle-draw"), | ||||||
|  | ] | ||||||
							
								
								
									
										45
									
								
								pinaxcon/raffle/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								pinaxcon/raffle/views.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | from django.apps import apps | ||||||
|  | from django.contrib.auth.decorators import login_required, user_passes_test | ||||||
|  | from django.http import HttpResponseRedirect | ||||||
|  | from django.shortcuts import render | ||||||
|  | from django.urls import reverse | ||||||
|  | from django.views.decorators.http import require_POST | ||||||
|  | 
 | ||||||
|  | from pinaxcon.raffle.models import Raffle, Prize | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _is_raffle_admin(user): | ||||||
|  |     group = apps.get_app_config('pinaxcon_raffle').get_admin_group() | ||||||
|  |     return group in user.groups.all() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @login_required | ||||||
|  | def raffle_view(request): | ||||||
|  |     raffles = Raffle.objects.all() | ||||||
|  |     for raffle in raffles: | ||||||
|  |         raffle.tickets = list(raffle.get_tickets(user=request.user)) | ||||||
|  | 
 | ||||||
|  |     return render(request, 'raffle.html', {'raffles': raffles}) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @login_required | ||||||
|  | @user_passes_test(_is_raffle_admin) | ||||||
|  | def draw_raffle_ticket(request, raffle_id=None): | ||||||
|  |     if request.method == 'POST' and raffle_id is not None: | ||||||
|  |         Raffle.objects.get(id=raffle_id).draw(user=request.user) | ||||||
|  |         return HttpResponseRedirect(reverse('raffle-draw')) | ||||||
|  | 
 | ||||||
|  |     raffles = Raffle.objects.prefetch_related('draws', 'prizes') | ||||||
|  |     return render(request, 'raffle_draw.html', {'raffles': raffles}) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @login_required | ||||||
|  | @user_passes_test(_is_raffle_admin) | ||||||
|  | @require_POST | ||||||
|  | def raffle_redraw(request, redraw_ticket_id): | ||||||
|  |     prize = Prize.objects.get(winning_ticket=redraw_ticket_id) | ||||||
|  |     prize.unlock(user=request.user) | ||||||
|  |     prize.remove_winner(user=request.user) | ||||||
|  |     prize.raffle.draw(user=request.user) | ||||||
|  |     return HttpResponseRedirect(reverse('raffle-draw')) | ||||||
|  | 
 | ||||||
|  | @ -240,6 +240,7 @@ INSTALLED_APPS = [ | ||||||
|     "pinaxcon", |     "pinaxcon", | ||||||
|     "pinaxcon.proposals", |     "pinaxcon.proposals", | ||||||
|     "pinaxcon.registrasion", |     "pinaxcon.registrasion", | ||||||
|  |     "pinaxcon.raffle", | ||||||
|     "jquery", |     "jquery", | ||||||
|     "djangoformsetjs", |     "djangoformsetjs", | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								pinaxcon/templates/raffle.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								pinaxcon/templates/raffle.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | {% extends "registrasion/base.html" %} | ||||||
|  | {% load registrasion_tags %} | ||||||
|  | {% load lca2018_tags %} | ||||||
|  | {% load staticfiles %} | ||||||
|  | 
 | ||||||
|  | {% block header_title %}{% conference_name %}{% endblock %} | ||||||
|  | 
 | ||||||
|  | {% block proposals_body %} | ||||||
|  | <h1 class="mb-5">Raffle Tickets</h1> | ||||||
|  | 
 | ||||||
|  | {% for raffle in raffles %} | ||||||
|  | {% if raffle.tickets %} | ||||||
|  |   <h2 class="mt-5">{{ raffle }}</h2> | ||||||
|  |    {% for id, numbers in raffle.tickets %} | ||||||
|  |     <h4 class="mt-3"><strong>Ticket {{ id }}</strong></h4> | ||||||
|  |     <p>{% for number in numbers %}{{ number }}{% if not forloop.last %}, {% endif %}{% endfor %}</p> | ||||||
|  |   {% endfor %} | ||||||
|  | {% endif %} | ||||||
|  | {% endfor %} | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										56
									
								
								pinaxcon/templates/raffle_draw.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								pinaxcon/templates/raffle_draw.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | ||||||
|  | {% extends "registrasion/base.html" %} | ||||||
|  | {% load registrasion_tags %} | ||||||
|  | {% load lca2018_tags %} | ||||||
|  | {% load staticfiles %} | ||||||
|  | 
 | ||||||
|  | {% block header_title %}{% conference_name %}{% endblock %} | ||||||
|  | 
 | ||||||
|  | {% block proposals_body %} | ||||||
|  | <h1 class="mb-5">Raffle Winners</h1> | ||||||
|  | 
 | ||||||
|  | {% for raffle in raffles %} | ||||||
|  | <h2 class="mt-5">{{ raffle }}</h2> | ||||||
|  | 
 | ||||||
|  | <dl class="row my-4"> | ||||||
|  |   {% for prize in raffle.prizes.all %} | ||||||
|  |   <dt class="col-sm-3 text-truncate">{{ prize }}</dt> | ||||||
|  |   <dd class="col-sm-9"> | ||||||
|  |     {% if prize.winning_ticket %} | ||||||
|  |     {% with prize.winning_ticket as winner %} | ||||||
|  |     {# this should be attendee name #} | ||||||
|  |     <p><strong>Winning ticket {{ winner.ticket }}, {{ winner.lineitem.invoice.user }}</strong><br /> | ||||||
|  |       Drawn by {{ winner.draw.drawn_by }}, {{ winner.draw.drawn_time}} | ||||||
|  |     </p> | ||||||
|  |     <div class="alert alert-danger"> | ||||||
|  |       <form method="POST" action="{% url 'raffle-redraw' winner.id %}"> | ||||||
|  |         {% csrf_token %} | ||||||
|  |         {# This should have a `reason` field that can be passed through to the Audit log #} | ||||||
|  |         <p> | ||||||
|  |           Re-draw <em>{{ prize }}</em> | ||||||
|  |           <button type="submit" class="btn btn-danger float-right">Re-draw</button> | ||||||
|  |         </p> | ||||||
|  |         <div class="clearfix"></div> | ||||||
|  |       </form> | ||||||
|  |     </div> | ||||||
|  |     {% endwith %} | ||||||
|  |     {% else %} | ||||||
|  |     Not drawn | ||||||
|  |     {% endif %} | ||||||
|  |   </dd> | ||||||
|  |   {% endfor %} | ||||||
|  | </dl> | ||||||
|  | 
 | ||||||
|  | {% if raffle.is_open %} | ||||||
|  | <form method="POST" action="{% url 'raffle-draw' raffle_id=raffle.id %}"> | ||||||
|  |     {% csrf_token %} | ||||||
|  |     <p class="text-center"> | ||||||
|  |       <button type="submit" class="btn btn-success">Draw tickets</button> | ||||||
|  |     </p> | ||||||
|  |     <div class="clearfix"></div> | ||||||
|  |   </form> | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  | {% if not forloop.last %}<hr>{% endif %} | ||||||
|  | {% endfor %} | ||||||
|  | 
 | ||||||
|  | {% endblock %} | ||||||
|  | @ -21,6 +21,7 @@ urlpatterns = [ | ||||||
|     url(r"^conference/", include("symposion.conference.urls")), |     url(r"^conference/", include("symposion.conference.urls")), | ||||||
| 
 | 
 | ||||||
|     url(r"^teams/", include("symposion.teams.urls")), |     url(r"^teams/", include("symposion.teams.urls")), | ||||||
|  |     url(r'^raffle/', include("pinaxcon.raffle.urls")), | ||||||
| 
 | 
 | ||||||
|     # Required by registrasion |     # Required by registrasion | ||||||
|     url(r'^tickets/payments/', include('registripe.urls')), |     url(r'^tickets/payments/', include('registripe.urls')), | ||||||
|  | @ -37,4 +38,4 @@ if settings.DEBUG: | ||||||
|     import debug_toolbar |     import debug_toolbar | ||||||
|     urlpatterns.insert(0, url(r'^__debug__/', include(debug_toolbar.urls))) |     urlpatterns.insert(0, url(r'^__debug__/', include(debug_toolbar.urls))) | ||||||
| 
 | 
 | ||||||
| urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)# | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Tobias S
						Tobias S