Fix #406 -- Allow moving waiting list entries to the top or bottom

This commit is contained in:
Raphael Michel
2018-08-07 10:53:07 +02:00
parent 0100604798
commit 67897dfcc0
7 changed files with 124 additions and 23 deletions

View File

@@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer):
class Meta: class Meta:
model = WaitingListEntry model = WaitingListEntry
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent') fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent', 'priority')
read_only_fields = ('id', 'created', 'voucher') read_only_fields = ('id', 'created', 'voucher')
def validate(self, data): def validate(self, data):

View File

@@ -0,0 +1,28 @@
# Generated by Django 2.1 on 2018-08-07 08:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0098_auto_20180731_1243'),
]
operations = [
migrations.AlterModelOptions(
name='waitinglistentry',
options={'ordering': ('-priority', 'created'), 'verbose_name': 'Waiting list entry', 'verbose_name_plural': 'Waiting list entries'},
),
migrations.AddField(
model_name='waitinglistentry',
name='priority',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='waitinglistentry',
name='voucher',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='waitinglistentries', to='pretixbase.Voucher', verbose_name='Assigned voucher'),
),
]

View File

@@ -65,11 +65,12 @@ class WaitingListEntry(LoggedModel):
max_length=190, max_length=190,
default='en' default='en'
) )
priority = models.IntegerField(default=0)
class Meta: class Meta:
verbose_name = _("Waiting list entry") verbose_name = _("Waiting list entry")
verbose_name_plural = _("Waiting list entries") verbose_name_plural = _("Waiting list entries")
ordering = ['created'] ordering = ('-priority', 'created')
def __str__(self): def __str__(self):
return '%s waits for %s' % (str(self.email), str(self.item)) return '%s waits for %s' % (str(self.email), str(self.item))

View File

@@ -22,7 +22,9 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None
qs = WaitingListEntry.objects.filter( qs = WaitingListEntry.objects.filter(
event=event, voucher__isnull=True event=event, voucher__isnull=True
).select_related('item', 'variation').prefetch_related('item__quotas', 'variation__quotas').order_by('created') ).select_related('item', 'variation').prefetch_related(
'item__quotas', 'variation__quotas'
).order_by('-priority', 'created')
if subevent_id and event.has_subevents: if subevent_id and event.has_subevents:
subevent = event.subevents.get(id=subevent_id) subevent = event.subevents.get(id=subevent_id)

View File

@@ -151,7 +151,15 @@
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<td>{{ e.subevent.name }} {{ e.subevent.get_date_range_display }}</td> <td>{{ e.subevent.name }} {{ e.subevent.get_date_range_display }}</td>
{% endif %} {% endif %}
<td>{{ e.created|date:"SHORT_DATETIME_FORMAT" }}</td> <td>
{{ e.created|date:"SHORT_DATETIME_FORMAT" }}
{% if e.priority != 0 %}
<span class="label label-info" data-toggle="tooltip"
title="{% trans "This entry has a modified priority. The higher this number is, the earlier this person will be assigned a voucher." %}">
{% if e.priority > 0 %}+{% endif %}{{ e.priority }}
</span>
{% endif %}
</td>
<td> <td>
{% if e.voucher %} {% if e.voucher %}
{% if e.voucher.redeemed >= e.voucher.max_usages %} {% if e.voucher.redeemed >= e.voucher.max_usages %}
@@ -184,8 +192,22 @@
</td> </td>
<td class="text-right"> <td class="text-right">
{% if not e.voucher %} {% if not e.voucher %}
<button name="move_top" value="{{ e.pk }}" class="btn btn-default btn-sm"
data-toggle="tooltip" title="{% trans "Move to the top of the list" %}">
<span class="fa fa-thumbs-up"></span>
</button>
<button name="move_end" value="{{ e.pk }}" class="btn btn-default btn-sm"
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
<span class="fa fa-thumbs-down"></span>
</button>
<a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a> <a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% else %} {% else %}
<button class="btn btn-default btn-sm disabled">
<span class="fa fa-thumbs-up"></span>
</button>
<button class="btn btn-default btn-sm disabled">
<span class="fa fa-thumbs-down"></span>
</button>
<span class="btn btn-danger btn-sm disabled"><i class="fa fa-trash"></i></span> <span class="btn btn-danger btn-sm disabled"><i class="fa fa-trash"></i></span>
{% endif %} {% endif %}
</td> </td>

View File

@@ -3,7 +3,7 @@ import io
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from django.db.models import F, Q, Sum from django.db.models import F, Max, Min, Q, Sum
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.http import Http404, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
@@ -52,14 +52,12 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
permission = 'can_view_orders' permission = 'can_view_orders'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
request=request):
messages.error(request, _('You do not have permission to do this'))
return self._redirect_back()
if 'assign' in request.POST: if 'assign' in request.POST:
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
request=request):
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: try:
wle = WaitingListEntry.objects.get( wle = WaitingListEntry.objects.get(
pk=request.POST.get('assign'), event=self.request.event, pk=request.POST.get('assign'), event=self.request.event,
@@ -71,18 +69,44 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
else: else:
messages.success(request, _('An email containing a voucher code has been sent to the ' messages.success(request, _('An email containing a voucher code has been sent to the '
'specified address.')) 'specified address.'))
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next"), allowed_hosts=None): return self._redirect_back()
return redirect(self.request.GET.get("next"))
return redirect(reverse('control:event.orders.waitinglist', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug
}))
except WaitingListEntry.DoesNotExist: except WaitingListEntry.DoesNotExist:
messages.error(request, _('Waiting list entry not found.')) messages.error(request, _('Waiting list entry not found.'))
return redirect(reverse('control:event.orders.waitinglist', kwargs={ return self._redirect_back()
'event': request.event.slug,
'organizer': request.event.organizer.slug if 'move_top' in request.POST:
})) try:
wle = WaitingListEntry.objects.get(
pk=request.POST.get('move_top'), event=self.request.event,
)
wle.priority = self.request.event.waitinglistentries.aggregate(m=Max('priority'))['m'] + 1
wle.save(update_fields=['priority'])
messages.success(request, _('The waiting list entry has been moved to the top.'))
return self._redirect_back()
except WaitingListEntry.DoesNotExist:
messages.error(request, _('Waiting list entry not found.'))
return self._redirect_back()
if 'move_end' in request.POST:
try:
wle = WaitingListEntry.objects.get(
pk=request.POST.get('move_end'), event=self.request.event,
)
wle.priority = self.request.event.waitinglistentries.aggregate(m=Min('priority'))['m'] - 1
wle.save(update_fields=['priority'])
messages.success(request, _('The waiting list entry has been moved to the end of the list.'))
return self._redirect_back()
except WaitingListEntry.DoesNotExist:
messages.error(request, _('Waiting list entry not found.'))
return self._redirect_back()
def _redirect_back(self):
if "next" in self.request.GET and is_safe_url(self.request.GET.get("next"), allowed_hosts=None):
return redirect(self.request.GET.get("next"))
return redirect(reverse('control:event.orders.waitinglist', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}))
def get_queryset(self): def get_queryset(self):
qs = WaitingListEntry.objects.filter( qs = WaitingListEntry.objects.filter(
@@ -172,7 +196,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
headers = [ headers = [
_('E-mail address'), _('Product'), _('On list since'), _('Status'), _('Voucher code'), _('E-mail address'), _('Product'), _('On list since'), _('Status'), _('Voucher code'),
_('Language') _('Language'), _('Priority')
] ]
if self.request.event.has_subevents: if self.request.event.has_subevents:
headers.append(pgettext('subevent', 'Date')) headers.append(pgettext('subevent', 'Date'))
@@ -201,6 +225,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
status, status,
w.voucher.code if w.voucher else '', w.voucher.code if w.voucher else '',
w.locale, w.locale,
str(w.priority)
] ]
if self.request.event.has_subevents: if self.request.event.has_subevents:
row.append(str(w.subevent)) row.append(str(w.subevent))

View File

@@ -104,6 +104,29 @@ def test_assign_single(client, env):
assert wle.voucher assert wle.voucher
@pytest.mark.django_db
def test_priority_single(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
wle = WaitingListEntry.objects.filter(voucher__isnull=True).last()
assert wle.priority == 0
client.post('/control/event/dummy/dummy/waitinglist/', {
'move_top': wle.pk
})
wle.refresh_from_db()
assert wle.priority == 1
client.post('/control/event/dummy/dummy/waitinglist/', {
'move_top': wle.pk
})
wle.refresh_from_db()
assert wle.priority == 2
client.post('/control/event/dummy/dummy/waitinglist/', {
'move_end': wle.pk
})
wle.refresh_from_db()
assert wle.priority == -1
@pytest.mark.django_db @pytest.mark.django_db
def test_delete_single(client, env): def test_delete_single(client, env):
client.login(email='dummy@dummy.dummy', password='dummy') client.login(email='dummy@dummy.dummy', password='dummy')