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.proposals", | ||||
|     "pinaxcon.registrasion", | ||||
|     "pinaxcon.raffle", | ||||
|     "jquery", | ||||
|     "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"^teams/", include("symposion.teams.urls")), | ||||
|     url(r'^raffle/', include("pinaxcon.raffle.urls")), | ||||
| 
 | ||||
|     # Required by registrasion | ||||
|     url(r'^tickets/payments/', include('registripe.urls')), | ||||
|  | @ -37,4 +38,4 @@ if settings.DEBUG: | |||
|     import debug_toolbar | ||||
|     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