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…
Reference in a new issue