Add waiting list

This commit is contained in:
Raphael Michel
2016-03-31 19:00:41 +02:00
parent 8f5849a90c
commit c83f539bba
29 changed files with 1216 additions and 26 deletions

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-06 20:27
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.base
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0050_orderposition_positionid_squashed_0061_event_location'),
]
operations = [
migrations.CreateModel(
name='WaitingListEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='On waiting list since')),
('email', models.EmailField(max_length=254, verbose_name='E-mail address')),
('locale', models.CharField(default='en', max_length=190)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Event', verbose_name='Event')),
('item', models.ForeignKey(help_text='The product the user waits for.', on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Item', verbose_name='Product')),
('variation', models.ForeignKey(blank=True, help_text='The variation of the product selected above.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.ItemVariation', verbose_name='Product variation')),
('voucher', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Voucher', verbose_name='Assigned voucher')),
],
options={
'ordering': ['created'],
'verbose_name': 'Waiting list entry',
'verbose_name_plural': 'Waiting list entries',
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
migrations.AlterField(
model_name='cachedcombinedticket',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@@ -19,3 +19,4 @@ from .orders import (
)
from .organizer import Organizer, OrganizerPermission, OrganizerSetting
from .vouchers import Voucher
from .waitinglist import WaitingListEntry

View File

@@ -231,7 +231,7 @@ class Item(LoggedModel):
return False
return True
def check_quotas(self, ignored_quotas=None, _cache=None):
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None):
"""
This method is used to determine whether this Item is currently available
for sale.
@@ -253,7 +253,7 @@ class Item(LoggedModel):
if self.variations.count() > 0: # NOQA
raise ValueError('Do not call this directly on items which have variations '
'but call this on their ItemVariation objects')
return min([q.availability(_cache=_cache) for q in check_quotas],
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
@cached_property
@@ -315,7 +315,7 @@ class ItemVariation(models.Model):
if self.item:
self.item.event.get_cache().clear()
def check_quotas(self, ignored_quotas=None, _cache=None) -> Tuple[int, int]:
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
"""
This method is used to determine whether this ItemVariation is currently
available for sale in terms of quotas.
@@ -324,6 +324,7 @@ class ItemVariation(models.Model):
quotas will be ignored in the calculation. If this leads
to no quotas being checked at all, this method will return
unlimited availability.
:param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
:returns: any of the return codes of :py:meth:`Quota.availability()`.
"""
check_quotas = set(self.quotas.all())
@@ -331,7 +332,7 @@ class ItemVariation(models.Model):
check_quotas -= set(ignored_quotas)
if not check_quotas:
return Quota.AVAILABILITY_OK, sys.maxsize
return min([q.availability(_cache=_cache) for q in check_quotas],
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
def __lt__(self, other):
@@ -534,7 +535,7 @@ class Quota(LoggedModel):
if self.event:
self.event.get_cache().clear()
def availability(self, now_dt: datetime=None, _cache=None) -> Tuple[int, int]:
def availability(self, now_dt: datetime=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
"""
This method is used to determine whether Items or ItemVariations belonging
to this quota should currently be available for sale.
@@ -542,14 +543,18 @@ class Quota(LoggedModel):
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
and the second is the number of available tickets.
"""
if _cache and count_waitinglist is not _cache.get('_count_waitinglist', True):
_cache.clear()
if _cache is not None and self.pk in _cache:
return _cache[self.pk]
res = self._availability(now_dt)
res = self._availability(now_dt, count_waitinglist)
if _cache is not None:
_cache[self.pk] = res
_cache['_count_waitinglist'] = count_waitinglist
return res
def _availability(self, now_dt: datetime=None):
def _availability(self, now_dt: datetime=None, count_waitinglist=True):
now_dt = now_dt or now()
size_left = self.size
if size_left is None:
@@ -572,6 +577,11 @@ class Quota(LoggedModel):
if size_left <= 0:
return Quota.AVAILABILITY_RESERVED, 0
if count_waitinglist:
size_left -= self.count_waiting_list_pending()
if size_left <= 0:
return Quota.AVAILABILITY_RESERVED, 0
return Quota.AVAILABILITY_OK, size_left
def count_blocking_vouchers(self, now_dt: datetime=None) -> int:
@@ -591,6 +601,13 @@ class Quota(LoggedModel):
free=Sum(Func(F('max_usages') - F('redeemed'), 0, function=func))
)['free'] or 0
def count_waiting_list_pending(self) -> int:
from pretix.base.models import WaitingListEntry
return WaitingListEntry.objects.filter(
Q(voucher__isnull=True) &
self._position_lookup
).distinct().count()
def count_in_cart(self, now_dt: datetime=None) -> int:
from pretix.base.models import CartPosition

View File

@@ -0,0 +1,130 @@
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.i18n import language
from pretix.base.models import Voucher
from pretix.base.services.mail import mail
from pretix.multidomain.urlreverse import build_absolute_uri
from .base import LoggedModel
from .event import Event
from .items import Item, ItemVariation
class WaitingListException(Exception):
pass
class WaitingListEntry(LoggedModel):
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="waitinglistentries",
verbose_name=_("Event"),
)
created = models.DateTimeField(
verbose_name=_("On waiting list since"),
auto_now_add=True
)
email = models.EmailField(
verbose_name=_("E-mail address")
)
voucher = models.ForeignKey(
'Voucher',
verbose_name=_("Assigned voucher"),
null=True, blank=True
)
item = models.ForeignKey(
Item, related_name='waitinglistentries',
verbose_name=_("Product"),
help_text=_(
"The product the user waits for."
)
)
variation = models.ForeignKey(
ItemVariation, related_name='waitinglistentries',
null=True, blank=True,
verbose_name=_("Product variation"),
help_text=_(
"The variation of the product selected above."
)
)
locale = models.CharField(
max_length=190,
default='en'
)
class Meta:
verbose_name = _("Waiting list entry")
verbose_name_plural = _("Waiting list entries")
ordering = ['created']
def __str__(self):
return '%s waits for %s' % (str(self.email), str(self.item))
def clean(self):
if WaitingListEntry.objects.filter(
item=self.item, variation=self.variation, email=self.email, voucher__isnull=True
).exclude(pk=self.pk).exists():
raise ValidationError(_('You are already on this waiting list! We will notify '
'you as soon as we have a ticket available for you.'))
if not self.variation and self.item.has_variations:
raise ValidationError(_('Please select a specific variation of this product.'))
def send_voucher(self, quota_cache=None):
availability = (
self.variation.check_quotas(count_waitinglist=False, _cache=quota_cache)
if self.variation
else self.item.check_quotas(count_waitinglist=False, _cache=quota_cache)
)
if availability[1] < 1:
raise WaitingListException(_('This product is currently not available.'))
if self.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
with transaction.atomic():
v = Voucher.objects.create(
event=self.event,
max_usages=1,
valid_until=now() + timedelta(hours=self.event.settings.waiting_list_hours),
item=self.item,
variation=self.variation,
tag='waiting-list',
comment=_('Automatically created from waiting list entry for {email}').format(
email=self.email
),
block_quota=True,
)
v.log_action('pretix.voucher.added.waitinglist', {
'item': self.item.pk,
'variation': self.variation.pk if self.variation else None,
'tag': 'waiting-list',
'block_quota': True,
'valid_until': v.valid_until.isoformat(),
'max_usages': 1,
'email': self.email,
'waitinglistentry': self.pk
})
self.log_action('pretix.waitinglist.voucher')
self.voucher = v
self.save()
with language(self.locale):
mail(
self.email,
_('You have been selected from the waitinglist for {event}').format(event=str(self.event)),
self.event.settings.mail_text_waiting_list,
{
'event': self.event.name,
'url': build_absolute_uri(self.event, 'presale:event.redeem') + '?voucher=' + self.voucher.code,
'code': self.voucher.code,
'product': str(self.item) + (' - ' + str(self.variation) if self.variation else ''),
'hours': self.event.settings.waiting_list_hours,
},
self.event,
locale=self.locale
)

View File

@@ -0,0 +1,56 @@
from django.dispatch import receiver
from pretix.base.models import Event, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.async import ProfiledTask
from pretix.base.signals import periodic_task
from pretix.celery_app import app
@app.task(base=ProfiledTask)
def assign_automatically(event_id: int):
event = Event.objects.get(id=event_id)
quota_cache = {}
gone = set()
qs = WaitingListEntry.objects.filter(
event=event, voucher__isnull=True
).select_related('item', 'variation').prefetch_related('item__quotas', 'variation__quotas').order_by('created')
sent = 0
for wle in qs:
if (wle.item, wle.variation) in gone:
continue
quotas = wle.variation.quotas.all() if wle.variation else wle.item.quotas.all()
availability = (
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache)
if wle.variation
else wle.item.check_quotas(count_waitinglist=False, _cache=quota_cache)
)
if availability[1] > 0:
try:
wle.send_voucher(quota_cache)
sent += 1
except WaitingListException: # noqa
continue
# Reduce affected quotas in cache
for q in quotas:
quota_cache[q.pk] = (
quota_cache[q.pk][0] if quota_cache[q.pk][0] > 1 else 0,
quota_cache[q.pk][1] - 1
)
else:
gone.add((wle.item, wle.variation))
return sent
@receiver(signal=periodic_task)
def process_waitinglist(sender, **kwargs):
qs = Event.objects.prefetch_related('setting_objects', 'organizer__setting_objects').select_related('organizer')
for e in qs:
if e.settings.waiting_list_enabled and e.settings.waiting_list_auto:
assign_automatically.apply_async(args=(e.pk,))

View File

@@ -132,6 +132,18 @@ DEFAULTS = {
'default': 'False',
'type': bool
},
'waiting_list_enabled': {
'default': 'False',
'type': bool
},
'waiting_list_auto': {
'default': 'True',
'type': bool
},
'waiting_list_hours': {
'default': '48',
'type': int
},
'ticket_download': {
'default': 'False',
'type': bool
@@ -258,6 +270,29 @@ your payment before {expire_date}.
You can view the payment information and the status of your order at
{url}
Best regards,
Your {event} team"""))
},
'mail_text_waiting_list': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
you submitted yourself to the waiting list for {event},
for the product {product}.
We now have a ticket ready for you! You can redeem it in our ticket shop
within the next {hours} hours by entering the following voucher code:
{code}
Alternatively, you can just click on the following link:
{url}
Please note that this link is only valid within the next {hours} hours!
We will reassign the ticket to the next person on the list if you do not
redeem the voucher within that timeframe.
Best regards,
Your {event} team"""))
},

View File

@@ -187,6 +187,27 @@ class EventSettingsForm(SettingsForm):
help_text=_("Publicly show how many tickets of a certain type are still available."),
required=False
)
waiting_list_enabled = forms.BooleanField(
label=_("Enable waiting list"),
help_text=_("Once a ticket is sold out, people can add themselves to a waiting list. As soon as a ticket "
"becomes available again, it will be reserved for the first person on the waiting list and this "
"person will receive an email notification with a voucher that can be used to buy a ticket."),
required=False
)
waiting_list_hours = forms.IntegerField(
label=_("Waiting list response time"),
min_value=6,
help_text=_("If a ticket voucher is sent to a person on the waiting list, it has to be redeemed within this "
"number of hours until it expires and can be re-assigned to the next person on the list."),
required=False
)
waiting_list_auto = forms.BooleanField(
label=_("Automatic waiting list assignments"),
help_text=_("If ticket capacity becomes free, automatically create a voucher and send it to the first person "
"on the waiting list for that product. If this is not active, mails will not be send automatically "
"but you can send them manually via the control panel."),
required=False
)
attendee_names_asked = forms.BooleanField(
label=_("Ask for attendee names"),
help_text=_("Ask for a name for all tickets which include admission to the event."),
@@ -433,6 +454,12 @@ class MailSettingsForm(SettingsForm):
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {expire_date}, {invoice_name}, {invoice_company}")
)
mail_text_waiting_list = I18nFormField(
label=_("Text"),
required=False,
widget=I18nTextarea,
help_text=_("Available placeholders: {event}, {url}, {product}, {hours}, {code}")
)
smtp_use_custom = forms.BooleanField(
label=_("Use custom SMTP server"),
help_text=_("All mail related to your event will be sent over the smtp server specified by you."),

View File

@@ -85,6 +85,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
'pretix.voucher.added': _('The voucher has been created.'),
'pretix.voucher.added.waitinglist': _('The voucher has been created and sent to a person on the waiting list.'),
'pretix.voucher.changed': _('The voucher has been modified.'),
'pretix.voucher.deleted': _('The voucher has been deleted.'),
'pretix.voucher.redeemed': _('The voucher has been redeemed in order {order_code}.'),
@@ -117,6 +118,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.')
}
data = json.loads(logentry.data)

View File

@@ -84,6 +84,12 @@
{% trans "Export" %}
</a>
</li>
<li>
<a href="{% url 'control:event.orders.waitinglist' organizer=request.event.organizer.slug event=request.event.slug %}"
{% if url_name == "event.orders.waitinglist" %}class="active"{% endif %}>
{% trans "Waiting list" %}
</a>
</li>
</ul>
</li>
{% endif %}

View File

@@ -99,6 +99,20 @@
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#waiting_list">
<strong>{% trans "Waiting list notification" %}</strong>
</a>
</h4>
</div>
<div id="waiting_list" class="panel-collapse collapse">
<div class="panel-body">
{% bootstrap_field form.mail_text_waiting_list layout="horizontal" %}
</div>
</div>
</div>
</div>
</fieldset>
<fieldset>

View File

@@ -42,6 +42,12 @@
{% bootstrap_field sform.attendee_names_required layout="horizontal" %}
{% bootstrap_field sform.cancel_allow_user layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Waiting list" %}</legend>
{% bootstrap_field sform.waiting_list_enabled layout="horizontal" %}
{% bootstrap_field sform.waiting_list_auto layout="horizontal" %}
{% bootstrap_field sform.waiting_list_hours layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -0,0 +1,145 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% block title %}{% trans "Waiting list" %}{% endblock %}
{% block content %}
<h1>{% trans "Waiting list" %}</h1>
{% if not request.event.settings.waiting_list_enabled %}
<div class="alert alert-danger">
{% trans "The waiting list is disabled, so if the event is sold out, people cannot add themselves to this list. If you want to enable it, go to the event settings." %}
</div>
{% endif %}
<div class="row">
{% if request.eventperm.can_change_orders %}
<form method="post" class="col-md-6"
action="{% url "control:event.orders.waitinglist.auto" event=request.event.slug organizer=request.organizer.slug %}"
data-asynctask>
<div class="panel panel-default">
<div class="panel-heading">
{% trans "Send vouchers" %}
</div>
<div class="panel-body">
{% csrf_token %}
{% if request.event.settings.waiting_list_auto %}
<p>
{% blocktrans trimmed %}
You have configured that vouchers will automatically be sent to the persons on this list who waited
the longest as soon as capacity becomes available. It might take up to half an hour for the
vouchers to be sent after the capacity is available, so don't worry if entries do not disappear
here immediately. If you want, you can also send them out manually right now.
{% endblocktrans %}
</p>
{% else %}
<p>
{% blocktrans trimmed %}
You have configured that vouchers will <strong>not</strong> be sent automatically.
You can either send them one-by-one in an order of your choice by clicking the
buttons next to a line in this table (if sufficient quota is available) or you can
press the big button below this text to send out as many vouchers as currently
possible to the persons who waitet longest.
{% endblocktrans %}
</p>
{% endif %}
<button class="btn btn-large btn-primary" type="submit">
{% trans "Send as many vouchers as possible" %}
</button>
</div>
</div>
</form>
{% endif %}
<div class="{% if request.eventperm.can_change_orders %}col-md-6{% else %}col-md-12{% endif %}">
<div class="panel panel-default">
<div class="panel-heading">
{% trans "Sales estimate" %}
</div>
<div class="panel-body">
{% blocktrans trimmed with amount=estimate|floatformat:2 currency=request.event.currency %}
If you can make enough room at your event to fit all the persons on the waiting list in, you
could sell tickets worth an additional <strong>{{ amount }} {{ currency }}</strong>.
{% endblocktrans %}
</div>
</div>
</div>
</div>
<p>
<form class="form-inline helper-display-inline" action="" method="get">
<select name="status" class="form-control">
<option value="a"
{% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "All entries" %}</option>
<option value="w"
{% if request.GET.status == "w" or not request.GET.status %}selected="selected"{% endif %}>{% trans "Waiting" %}</option>
<option value="s"
{% if request.GET.status == "s" %}selected="selected"{% endif %}>{% trans "Voucher assigned" %}</option>
</select>
<select name="item" class="form-control">
<option value="">{% trans "All products" %}</option>
{% for item in items %}
<option value="{{ item.id }}"
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
{{ item.name }}
</option>
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</form>
</p>
<form method="post" action="">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "User" %}</th>
<th>{% trans "Product" %}</th>
<th>{% trans "On the list since" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Voucher" %}</th>
</tr>
</thead>
<tbody>
{% for e in entries %}
<tr>
<td>{{ e.email }}</td>
<td>
{{ e.item }}
{% if e.variation %}
{{ e.variation }}
{% endif %}
</td>
<td>{{ e.created|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>
{% if e.voucher %}
<span class="label label-success">{% trans "Voucher assigned" %}</span>
{% elif e.availability.0 == 100 %}
<span class="label label-warning">
{% blocktrans with num=e.availability.1 %}
Waiting, product {{ num }}x available
{% endblocktrans %}
</span>
{% else %}
<span class="label label-danger">{% trans "Waiting, product unavailable" %}</span>
{% endif %}
</td>
<td>
{% if e.voucher %}
<a href="{% url "control:event.voucher" organizer=request.event.organizer.slug event=request.event.slug voucher=e.voucher.pk %}">
{{ e.voucher }}
</a>
{% elif not e.voucher and e.availability.0 == 100 %}
<button name="assign" value="{{ e.pk }}" class="btn btn-default btn-xs">
{% trans "Send a voucher" %}
</button>
{% endif %}
</td>
<td class="text-right">
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -2,7 +2,7 @@ from django.conf.urls import include, url
from pretix.control.views import (
auth, dashboards, event, global_settings, help, item, main, orders,
organizer, user, vouchers,
organizer, user, vouchers, waitinglist,
)
urlpatterns = [
@@ -121,6 +121,8 @@ urlpatterns = [
url(r'^orders/export/$', orders.ExportView.as_view(), name='event.orders.export'),
url(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
url(r'^waitinglist/$', waitinglist.WaitingListView.as_view(), name='event.orders.waitinglist'),
url(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
])),
url(r'^help/(?P<topic>[a-zA-Z0-9_/]+)$', help.HelpView.as_view(), name='help'),
]

View File

@@ -12,7 +12,9 @@ from django.utils import formats
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, Item, Order, OrderPosition, Voucher
from pretix.base.models import (
Event, Item, Order, OrderPosition, Voucher, WaitingListEntry,
)
from pretix.control.signals import (
event_dashboard_widgets, user_dashboard_widgets,
)
@@ -85,9 +87,53 @@ def base_widgets(sender, **kwargs):
]
@receiver(signal=event_dashboard_widgets)
def waitinglist_widgets(sender, **kwargs):
widgets = []
wles = WaitingListEntry.objects.filter(event=sender)
if wles.count():
quota_cache = {}
itemvar_cache = {}
happy = 0
for wle in wles:
if (wle.item, wle.variation) not in itemvar_cache:
itemvar_cache[(wle.item, wle.variation)] = (
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache)
if wle.variation
else wle.item.check_quotas(count_waitinglist=False, _cache=quota_cache)
)
row = itemvar_cache.get((wle.item, wle.variation))
if row[1] > 0:
itemvar_cache[(wle.item, wle.variation)] = (row[0], row[1] - 1)
happy += 1
widgets.append({
'content': NUM_WIDGET.format(num=str(happy), text=_('available to give to people on waiting list')),
'priority': 50,
'url': reverse('control:event.orders.waitinglist', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug,
})
})
widgets.append({
'content': NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')),
'display_size': 'small',
'priority': 50,
'url': reverse('control:event.orders.waitinglist', kwargs={
'event': sender.slug,
'organizer': sender.organizer.slug,
})
})
return widgets
@receiver(signal=event_dashboard_widgets)
def quota_widgets(sender, **kwargs):
widgets = []
for q in sender.quotas.all():
status, left = q.availability()
widgets.append({

View File

@@ -607,24 +607,33 @@ class QuotaView(ChartContainingView, DetailView):
data = [
{
'label': ugettext('Paid orders'),
'value': self.object.count_paid_orders()
'value': self.object.count_paid_orders(),
'sum': True,
},
{
'label': ugettext('Pending orders'),
'value': self.object.count_pending_orders()
'value': self.object.count_pending_orders(),
'sum': True,
},
{
'label': ugettext('Vouchers'),
'value': self.object.count_blocking_vouchers()
'value': self.object.count_blocking_vouchers(),
'sum': True,
},
{
'label': ugettext('Current user\'s carts'),
'value': self.object.count_in_cart()
}
'value': self.object.count_in_cart(),
'sum': True,
},
{
'label': ugettext('Waiting list'),
'value': self.object.count_waiting_list_pending(),
'sum': False,
},
]
ctx['quota_table_rows'] = list(data)
sum_values = sum([d['value'] for d in data])
sum_values = sum([d['value'] for d in data if d['sum']])
if self.object.size is not None:
data.append({

View File

@@ -0,0 +1,127 @@
from django.contrib import messages
from django.db.models import Sum
from django.db.models.functions import Coalesce
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.views import View
from django.views.generic import ListView
from pretix.base.models import Item, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.waitinglist import assign_automatically
from pretix.base.views.async import AsyncAction
from pretix.control.permissions import EventPermissionRequiredMixin
class AutoAssign(EventPermissionRequiredMixin, AsyncAction, View):
task = assign_automatically
known_errortypes = ['WaitingListError']
permission = 'can_change_orders'
def get_success_message(self, value):
return _('{num} vouchers have been created and sent out via email.').format(num=value)
def get_success_url(self, value):
return self.get_error_url()
def get_error_url(self):
return reverse('control:event.orders.waitinglist', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
})
def post(self, request, *args, **kwargs):
return self.do(self.request.event.id)
class WaitingListView(EventPermissionRequiredMixin, ListView):
model = WaitingListEntry
context_object_name = 'entries'
paginate_by = 30
template_name = 'pretixcontrol/waitinglist/index.html'
permission = 'can_view_orders'
def post(self, request, *args, **kwargs):
if 'assign' in request.POST:
if not request.eventperm.can_change_orders:
messages.error(request, _('You do not have permission to do this'))
return redirect(reverse('control:event.orders.waitinglist', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug
}))
try:
wle = WaitingListEntry.objects.get(
pk=request.POST.get('assign'), event=self.request.event,
)
try:
wle.send_voucher()
except WaitingListException as e:
messages.error(request, str(e))
else:
messages.success(request, _('An email containing a voucher code has been sent to the '
'specified address.'))
return redirect(reverse('control:event.orders.waitinglist', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug
}))
except WaitingListEntry.DoesNotExist:
messages.error(request, _('Waiting list entry not found.'))
return redirect(reverse('control:event.orders.waitinglist', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug
}))
def get_queryset(self):
qs = WaitingListEntry.objects.filter(
event=self.request.event
).select_related('item', 'variation', 'voucher').prefetch_related('item__quotas', 'variation__quotas')
s = self.request.GET.get("status", "")
if s == 's':
qs = qs.filter(voucher__isnull=False)
elif s == 'a':
pass
else:
qs = qs.filter(voucher__isnull=True)
if self.request.GET.get("item", "") != "":
i = self.request.GET.get("item", "")
qs = qs.filter(item_id__in=(i,))
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['items'] = Item.objects.filter(event=self.request.event)
ctx['filtered'] = ("status" in self.request.GET or "item" in self.request.GET)
itemvar_cache = {}
quota_cache = {}
any_avail = False
for wle in ctx[self.context_object_name]:
if (wle.item, wle.variation) in itemvar_cache:
wle.availability = itemvar_cache.get((wle.item, wle.variation))
else:
wle.availability = (
wle.variation.check_quotas(count_waitinglist=False, _cache=quota_cache)
if wle.variation
else wle.item.check_quotas(count_waitinglist=False, _cache=quota_cache)
)
itemvar_cache[(wle.item, wle.variation)] = wle.availability
if wle.availability[0] == 100:
any_avail = True
ctx['any_avail'] = any_avail
ctx['estimate'] = self.get_sales_estimate()
return ctx
def get_sales_estimate(self):
qs = WaitingListEntry.objects.filter(
event=self.request.event, voucher__isnull=True
).aggregate(
s=Sum(
Coalesce('variation__default_price', 'item__default_price')
)
)
return qs['s']

View File

@@ -0,0 +1,13 @@
from django import forms
from pretix.base.models import WaitingListEntry
class WaitingListForm(forms.ModelForm):
class Meta:
model = WaitingListEntry
fields = ('email',)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)

View File

@@ -1,13 +1,28 @@
{% load i18n %}
{% load eventurl %}
{% if avail == 0 %}
<div class="col-md-2 col-xs-6 availability-box gone">
<strong>{% trans "SOLD OUT" %}</strong>
{% if event.settings.waiting_list_enabled %}
<br/>
<a href="{% eventurl event "presale:event.waitinglist" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}">
<span class="fa fa-plus-circle"></span>
{% trans "Waiting list" %}
</a>
{% endif %}
</div>
{% elif avail < 100 %}
<div class="col-md-2 col-xs-6 availability-box unavailable">
<strong>{% trans "Reserved" %}</strong><br />
<small>
{% trans "All remaining products are reserved but might become available again." %}
</small>
<strong>{% trans "Reserved" %}
<span class="fa fa-info-circle" data-toggle="tooltip"
title="{% trans "All remaining products are reserved but might become available again." %}"></span>
</strong>
{% if event.settings.waiting_list_enabled %}
<br/>
<a href="{% eventurl event "presale:event.waitinglist" %}?item={{ item.pk }}{% if var %}&var={{ var.pk }}{% endif %}">
<span class="fa fa-plus-circle"></span>
{% trans "Waiting list" %}
</a>
{% endif %}
</div>
{% endif %}

View File

@@ -156,7 +156,7 @@
name="variation_{{ item.id }}_{{ var.id }}">
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 %}
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability.0 event=event item=item var=var %}
{% endif %}
<div class="clearfix"></div>
</div>
@@ -211,7 +211,7 @@
max="{{ item.order_max }}" name="item_{{ item.id }}">
</div>
{% else %}
{% include "pretixpresale/event/fragment_availability.html" with avail=item.cached_availability.0 %}
{% include "pretixpresale/event/fragment_availability.html" with avail=item.cached_availability.0 event=event item=item var=0 %}
{% endif %}
<div class="clearfix"></div>
</div>

View File

@@ -0,0 +1,31 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n eventurl thumbnail bootstrap3 %}
{% block title %}{% trans "Waiting list" %}{% endblock %}
{% block content %}
<h2>{% trans "Add me to the waiting list" %}</h2>
<form action="" method="post">
{% csrf_token %}
<div class="form-horizontal">
<div class="form-group">
<label class="col-md-3 control-label" for="id_email">{% trans "Product" %}</label>
<div class="col-md-9">
<input class="form-control" readonly="readonly"
value="{{ item.name }}{% if variation %} {{ variation.value }}{% endif %}">
</div>
</div>
{% bootstrap_form form layout='horizontal' %}
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<div class="help-block">
{% blocktrans trimmed %}
If tickets become available again, we will inform the first persons on the waiting list. If we notify you, you'll have 48 hours time to buy a ticket until we assign it to the next person on the list.
{% endblocktrans %}
</div>
<button type="submit" class="btn btn-primary">
{% trans "Add me to the list" %}
</button>
</div>
</div>
</div>
</form>
{% endblock %}

View File

@@ -7,6 +7,7 @@ import pretix.presale.views.locale
import pretix.presale.views.order
import pretix.presale.views.organizer
import pretix.presale.views.user
import pretix.presale.views.waiting
# This is not a valid Django URL configuration, as the final
# configuration is done by the pretix.multidomain package.
@@ -14,6 +15,7 @@ import pretix.presale.views.user
event_patterns = [
url(r'^cart/add$', pretix.presale.views.cart.CartAdd.as_view(), name='event.cart.add'),
url(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
url(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'),
url(r'^checkout/start$', pretix.presale.views.checkout.CheckoutView.as_view(), name='event.checkout.start'),
url(r'^redeem$', pretix.presale.views.cart.RedeemView.as_view(),
name='event.redeem'),

View File

@@ -0,0 +1,76 @@
from django.contrib import messages
from django.shortcuts import redirect
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView
from ...base.models import Item, ItemVariation, WaitingListEntry
from ...multidomain.urlreverse import eventreverse
from ..forms.waitinglist import WaitingListForm
class WaitingView(FormView):
template_name = 'pretixpresale/event/waitinglist.html'
form_class = WaitingListForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
kwargs['instance'] = WaitingListEntry(
item=self.item_and_variation[0], variation=self.item_and_variation[1],
event=self.request.event, locale=translation.get_language()
)
return kwargs
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['event'] = self.request.event
ctx['item'], ctx['variation'] = self.item_and_variation
return ctx
@cached_property
def item_and_variation(self):
try:
item = self.request.event.items.get(pk=self.request.GET.get('item'))
if 'var' in self.request.GET:
var = item.variations.get(pk=self.request.GET['var'])
elif item.has_variations:
return None
else:
var = None
return item, var
except (Item.DoesNotExist, ItemVariation.DoesNotExist):
return None
def dispatch(self, request, *args, **kwargs):
self.request = request
if not self.request.event.settings.waiting_list_enabled:
messages.error(request, _("Waiting lists are disabled for this event."))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
if not self.item_and_variation:
messages.error(request, _("We could not identify the product you selected."))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
availability = (
self.item_and_variation[1].check_quotas(count_waitinglist=False)
if self.item_and_variation[1]
else self.item_and_variation[0].check_quotas(count_waitinglist=False)
)
if availability[0] == 100:
messages.error(self.request, _("You cannot add yourself to the waiting list as this product ist currently "
"available."))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
form.save()
messages.success(self.request, _("We've added you to the waiting list. You will receive "
"an email as soon as tickets get available again."))
return super().form_valid(form)
def get_success_url(self):
return eventreverse(self.request.event, 'presale:event.index')

View File

@@ -33,6 +33,8 @@ $(function () {
$("#voucher-box").slideDown();
$("#voucher-toggle").slideUp();
});
$('[data-toggle="tooltip"]').tooltip();
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);

View File

@@ -13,7 +13,7 @@ from django.utils.timezone import now
from pretix.base.models import (
CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order,
OrderPosition, Organizer, Question, Quota, User, Voucher,
OrderPosition, Organizer, Question, Quota, User, Voucher, WaitingListEntry,
)
from pretix.base.services.orders import (
OrderError, cancel_order, mark_order_paid, perform_order,
@@ -316,6 +316,104 @@ class QuotaTestCase(BaseQuotaTestCase):
self.assertEqual(self.quota.count_in_cart(), 1)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
def test_waitinglist_item_active(self):
self.quota.items.add(self.item1)
self.quota.size = 1
self.quota.save()
WaitingListEntry.objects.create(
event=self.event, item=self.item1, email='foo@bar.com'
)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_RESERVED, 0))
self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
def test_waitinglist_variation_active(self):
self.quota.variations.add(self.var1)
self.quota.size = 1
self.quota.save()
WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com'
)
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_RESERVED, 0))
self.assertEqual(self.var1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
def test_waitinglist_variation_fulfilled(self):
self.quota.variations.add(self.var1)
self.quota.size = 1
self.quota.save()
v = Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, redeemed=1)
WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com', voucher=v
)
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
self.assertEqual(self.var1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
def test_waitinglist_variation_other(self):
self.quota.variations.add(self.var1)
self.quota.size = 1
self.quota.save()
WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var2, email='foo@bar.com'
)
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
self.assertEqual(self.var1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
def test_quota_cache(self):
self.quota.variations.add(self.var1)
self.quota.size = 1
self.quota.save()
WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com'
)
cache = {}
self.assertEqual(self.var1.check_quotas(_cache=cache), (Quota.AVAILABILITY_RESERVED, 0))
with self.assertNumQueries(1):
self.assertEqual(self.var1.check_quotas(_cache=cache), (Quota.AVAILABILITY_RESERVED, 0))
# Do not reuse cache for count_waitinglist=False
self.assertEqual(self.var1.check_quotas(_cache=cache, count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
with self.assertNumQueries(1):
self.assertEqual(self.var1.check_quotas(_cache=cache, count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
class WaitingListTestCase(BaseQuotaTestCase):
def test_duplicate(self):
w1 = WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com'
)
w1.clean()
w2 = WaitingListEntry(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com'
)
with self.assertRaises(ValidationError):
w2.clean()
def test_duplicate_of_successful(self):
v = Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, redeemed=1)
w1 = WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com',
voucher=v
)
w1.clean()
w2 = WaitingListEntry(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com'
)
w2.clean()
def test_missing_variation(self):
w2 = WaitingListEntry(
event=self.event, item=self.item2, email='foo@bar.com'
)
with self.assertRaises(ValidationError):
w2.clean()
class VoucherTestCase(BaseQuotaTestCase):
def test_voucher_reuse(self):
self.quota.items.add(self.item1)
v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() + timedelta(days=5))
@@ -396,9 +494,6 @@ class QuotaTestCase(BaseQuotaTestCase):
v = Voucher(variation=self.var1, event=self.event)
v.clean()
class VoucherTestCase(BaseQuotaTestCase):
def test_calculate_price_none(self):
v = Voucher.objects.create(event=self.event, price_mode='none', value=Decimal('10.00'))
v.calculate_price(Decimal('23.42')) == Decimal('23.42')

View File

@@ -0,0 +1,132 @@
from django.core import mail as djmail
from django.test import TestCase
from django.utils.timezone import now
from pretix.base.models import (
Event, Item, ItemVariation, Organizer, Quota, Voucher, WaitingListEntry,
)
from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.waitinglist import (
assign_automatically, process_waitinglist,
)
class WaitingListTestCase(TestCase):
@classmethod
def setUpTestData(cls):
o = Organizer.objects.create(name='Dummy', slug='dummy')
cls.event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(),
)
def setUp(self):
djmail.outbox = []
self.quota = Quota.objects.create(name="Test", size=2, event=self.event)
self.item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
admission=True)
self.item2 = Item.objects.create(event=self.event, name="T-Shirt", default_price=23)
self.item3 = Item.objects.create(event=self.event, name="Goodie", default_price=23)
self.var1 = ItemVariation.objects.create(item=self.item2, value='S')
self.var2 = ItemVariation.objects.create(item=self.item2, value='M')
self.var3 = ItemVariation.objects.create(item=self.item3, value='Fancy')
def test_send_unavailable(self):
self.quota.items.add(self.item1)
self.quota.size = 0
self.quota.save()
wle = WaitingListEntry.objects.create(
event=self.event, item=self.item1, email='foo@bar.com'
)
with self.assertRaises(WaitingListException):
wle.send_voucher()
def test_send_double(self):
self.quota.variations.add(self.var1)
self.quota.size = 1
self.quota.save()
v = Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True, redeemed=1)
wle = WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com', voucher=v
)
with self.assertRaises(WaitingListException):
wle.send_voucher()
def test_send_variation(self):
wle = WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com'
)
wle.send_voucher()
wle.refresh_from_db()
assert wle.voucher
assert wle.voucher.item == wle.item
assert wle.voucher.variation == wle.variation
assert wle.email in wle.voucher.comment
assert wle.voucher.block_quota
assert wle.voucher.max_usages == 1
assert wle.voucher.event == self.event
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [wle.email]
def test_send_custom_validity(self):
self.event.settings.set('waiting_list_hours', 24)
wle = WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo@bar.com'
)
wle.send_voucher()
wle.refresh_from_db()
assert 3600 * 23 < (wle.voucher.valid_until - now()).seconds < 3600 * 24
def test_send_auto(self):
self.quota.variations.add(self.var1)
self.quota.size = 7
self.quota.save()
for i in range(10):
WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo{}@bar.com'.format(i)
)
WaitingListEntry.objects.create(
event=self.event, item=self.item1, email='bar{}@bar.com'.format(i)
)
assign_automatically.apply(args=(self.event.pk,))
assert WaitingListEntry.objects.filter(voucher__isnull=True).count() == 3
assert Voucher.objects.count() == 17
assert sorted(list(WaitingListEntry.objects.filter(voucher__isnull=True).values_list('email', flat=True))) == [
'foo7@bar.com', 'foo8@bar.com', 'foo9@bar.com'
]
def test_send_periodic(self):
self.event.settings.set('waiting_list_enabled', True)
self.event.settings.set('waiting_list_auto', True)
for i in range(5):
WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo{}@bar.com'.format(i)
)
process_waitinglist(None)
assert Voucher.objects.count() == 5
def test_send_periodic_disabled(self):
self.event.settings.set('waiting_list_enabled', True)
self.event.settings.set('waiting_list_auto', False)
for i in range(5):
WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo{}@bar.com'.format(i)
)
process_waitinglist(None)
assert WaitingListEntry.objects.filter(voucher__isnull=True).count() == 5
assert Voucher.objects.count() == 0
def test_send_periodic_disabled2(self):
self.event.settings.set('waiting_list_enabled', False)
self.event.settings.set('waiting_list_auto', True)
for i in range(5):
WaitingListEntry.objects.create(
event=self.event, item=self.item2, variation=self.var1, email='foo{}@bar.com'.format(i)
)
process_waitinglist(None)
assert WaitingListEntry.objects.filter(voucher__isnull=True).count() == 5
assert Voucher.objects.count() == 0

View File

@@ -67,6 +67,8 @@ event_urls = [
"orders/ABC/contact",
"orders/ABC/",
"orders/",
"waitinglist/",
"waitinglist/auto_assign",
"invoice/1",
]
@@ -154,6 +156,8 @@ event_permission_urls = [
("can_view_vouchers", "vouchers/tags/", 200),
("can_change_vouchers", "vouchers/1234/", 404),
("can_change_vouchers", "vouchers/1234/delete", 404),
("can_view_orders", "waitinglist/", 200),
("can_change_orders", "waitinglist/auto_assign", 405),
]

View File

@@ -123,6 +123,8 @@ def logged_in_client(client, event):
('/control/event/{orga}/{event}/orders/export/', 200),
('/control/event/{orga}/{event}/orders/go', 302),
('/control/event/{orga}/{event}/orders/', 200),
('/control/event/{orga}/{event}/waitinglist/', 200),
('/control/event/{orga}/{event}/waitinglist/auto_assign', 405),
])
@pytest.mark.django_db
def test_one_view(logged_in_client, url, expected, event, item, item_category, order, question, quota, voucher):

View File

@@ -0,0 +1,87 @@
import pytest
from django.utils.timezone import now
from pretix.base.models import (
Event, EventPermission, Item, Organizer, Quota, User, Voucher,
WaitingListEntry,
)
from pretix.control.views.dashboards import waitinglist_widgets
@pytest.fixture
def env():
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now(), plugins='pretix.plugins.banktransfer,tests.testdummy'
)
event.settings.set('ticketoutput_testdummy__enabled', True)
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
item1 = Item.objects.create(event=event, name="Ticket", default_price=23,
admission=True)
item2 = Item.objects.create(event=event, name="Ticket", default_price=23,
admission=True)
for i in range(5):
WaitingListEntry.objects.create(
event=event, item=item1, email='foo{}@bar.com'.format(i)
)
v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=1)
WaitingListEntry.objects.create(
event=event, item=item1, email='success@example.org', voucher=v
)
WaitingListEntry.objects.create(
event=event, item=item2, email='item2@example.org'
)
EventPermission.objects.create(
event=event,
user=user,
can_view_orders=True,
can_change_orders=True
)
return event, user, o, item1
@pytest.mark.django_db
def test_list(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert 'success@example.org' not in response.rendered_content
assert 'item2@example.org' in response.rendered_content
assert 'foo0@bar.com' in response.rendered_content
assert response.context['estimate'] == 23 * 6
response = client.get('/control/event/dummy/dummy/waitinglist/?status=a')
assert 'success@example.org' in response.rendered_content
assert 'foo0@bar.com' in response.rendered_content
response = client.get('/control/event/dummy/dummy/waitinglist/?status=s')
assert 'success@example.org' in response.rendered_content
assert 'foo0@bar.com' not in response.rendered_content
response = client.get('/control/event/dummy/dummy/waitinglist/?item=%d' % env[3].pk)
assert 'item2@example.org' not in response.rendered_content
assert 'foo0@bar.com' in response.rendered_content
@pytest.mark.django_db
def test_assign_single(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
wle = WaitingListEntry.objects.filter(voucher__isnull=True).last()
client.post('/control/event/dummy/dummy/waitinglist/', {
'assign': wle.pk
})
wle.refresh_from_db()
assert wle.voucher
@pytest.mark.django_db
def test_dashboard(client, env):
quota = Quota.objects.create(name="Test", size=2, event=env[0])
quota.items.add(env[3])
w = waitinglist_widgets(env[0])
assert '3' in w[0]['content']
assert '7' in w[1]['content']

View File

@@ -9,7 +9,7 @@ from tests.base import SoupTest
from pretix.base.models import (
Event, EventPermission, Item, ItemCategory, ItemVariation, Order,
Organizer, Quota, User,
Organizer, Quota, User, WaitingListEntry,
)
@@ -343,6 +343,72 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
assert "alert-danger" in html.rendered_content
class WaitingListTest(EventTestMixin, SoupTest):
def setUp(self):
super().setUp()
self.q = Quota.objects.create(event=self.event, name='Quota', size=0)
self.v = self.event.vouchers.create(quota=self.q)
self.item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=Decimal('12.00'),
active=True)
self.q.items.add(self.item)
self.event.settings.set('waiting_list_enabled', True)
def test_disabled(self):
self.event.settings.set('waiting_list_enabled', False)
response = self.client.get(
'/%s/%s/' % (self.orga.slug, self.event.slug)
)
self.assertEqual(response.status_code, 200)
self.assertNotIn('waitinglist', response.rendered_content)
response = self.client.get(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk + 1)
)
self.assertEqual(response.status_code, 302)
def test_display_link(self):
response = self.client.get(
'/%s/%s/' % (self.orga.slug, self.event.slug)
)
self.assertEqual(response.status_code, 200)
self.assertIn('waitinglist', response.rendered_content)
def test_submit_form(self):
response = self.client.get(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk)
)
self.assertEqual(response.status_code, 200)
self.assertIn('waiting list', response.rendered_content)
response = self.client.post(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk), {
'email': 'foo@bar.com'
}
)
self.assertEqual(response.status_code, 302)
wle = WaitingListEntry.objects.get(email='foo@bar.com')
assert wle.event == self.event
assert wle.item == self.item
assert wle.variation is None
assert wle.voucher is None
assert wle.locale == 'en'
def test_invalid_item(self):
response = self.client.get(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk + 1)
)
self.assertEqual(response.status_code, 302)
def test_available(self):
self.q.size = 1
self.q.save()
response = self.client.post(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk), {
'email': 'foo@bar.com'
}
)
self.assertEqual(response.status_code, 302)
self.assertFalse(WaitingListEntry.objects.filter(email='foo@bar.com').exists())
class DeadlineTest(EventTestMixin, TestCase):
def setUp(self):
super().setUp()