From 67897dfcc0c7abb81d1e82621de18cc9a8ec00ab Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 7 Aug 2018 10:53:07 +0200 Subject: [PATCH] Fix #406 -- Allow moving waiting list entries to the top or bottom --- src/pretix/api/serializers/waitinglist.py | 2 +- .../migrations/0099_auto_20180807_0841.py | 28 +++++++++ src/pretix/base/models/waitinglist.py | 3 +- src/pretix/base/services/waitinglist.py | 4 +- .../pretixcontrol/waitinglist/index.html | 24 ++++++- src/pretix/control/views/waitinglist.py | 63 +++++++++++++------ src/tests/control/test_waitinglist.py | 23 +++++++ 7 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 src/pretix/base/migrations/0099_auto_20180807_0841.py diff --git a/src/pretix/api/serializers/waitinglist.py b/src/pretix/api/serializers/waitinglist.py index ed0ca9dd04..8be870d985 100644 --- a/src/pretix/api/serializers/waitinglist.py +++ b/src/pretix/api/serializers/waitinglist.py @@ -8,7 +8,7 @@ class WaitingListSerializer(I18nAwareModelSerializer): class Meta: 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') def validate(self, data): diff --git a/src/pretix/base/migrations/0099_auto_20180807_0841.py b/src/pretix/base/migrations/0099_auto_20180807_0841.py new file mode 100644 index 0000000000..8b1c4d4295 --- /dev/null +++ b/src/pretix/base/migrations/0099_auto_20180807_0841.py @@ -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'), + ), + ] diff --git a/src/pretix/base/models/waitinglist.py b/src/pretix/base/models/waitinglist.py index 886b97be79..c2b49883a3 100644 --- a/src/pretix/base/models/waitinglist.py +++ b/src/pretix/base/models/waitinglist.py @@ -65,11 +65,12 @@ class WaitingListEntry(LoggedModel): max_length=190, default='en' ) + priority = models.IntegerField(default=0) class Meta: verbose_name = _("Waiting list entry") verbose_name_plural = _("Waiting list entries") - ordering = ['created'] + ordering = ('-priority', 'created') def __str__(self): return '%s waits for %s' % (str(self.email), str(self.item)) diff --git a/src/pretix/base/services/waitinglist.py b/src/pretix/base/services/waitinglist.py index b71c711609..e1e81f7dcb 100644 --- a/src/pretix/base/services/waitinglist.py +++ b/src/pretix/base/services/waitinglist.py @@ -22,7 +22,9 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None qs = WaitingListEntry.objects.filter( 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: subevent = event.subevents.get(id=subevent_id) diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html index 01800aa796..561ed19003 100644 --- a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html @@ -151,7 +151,15 @@ {% if request.event.has_subevents %} {{ e.subevent.name }} – {{ e.subevent.get_date_range_display }} {% endif %} - {{ e.created|date:"SHORT_DATETIME_FORMAT" }} + + {{ e.created|date:"SHORT_DATETIME_FORMAT" }} + {% if e.priority != 0 %} + + {% if e.priority > 0 %}+{% endif %}{{ e.priority }} + + {% endif %} + {% if e.voucher %} {% if e.voucher.redeemed >= e.voucher.max_usages %} @@ -184,8 +192,22 @@ {% if not e.voucher %} + + {% else %} + + {% endif %} diff --git a/src/pretix/control/views/waitinglist.py b/src/pretix/control/views/waitinglist.py index 07a2bedef2..9f047972ac 100644 --- a/src/pretix/control/views/waitinglist.py +++ b/src/pretix/control/views/waitinglist.py @@ -3,7 +3,7 @@ import io from django.contrib import messages 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.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import redirect @@ -52,14 +52,12 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView): permission = 'can_view_orders' 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 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: wle = WaitingListEntry.objects.get( pk=request.POST.get('assign'), event=self.request.event, @@ -71,18 +69,44 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView): else: messages.success(request, _('An email containing a voucher code has been sent to the ' 'specified address.')) - 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': request.event.slug, - 'organizer': request.event.organizer.slug - })) + return self._redirect_back() 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 - })) + return self._redirect_back() + + 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): qs = WaitingListEntry.objects.filter( @@ -172,7 +196,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView): headers = [ _('E-mail address'), _('Product'), _('On list since'), _('Status'), _('Voucher code'), - _('Language') + _('Language'), _('Priority') ] if self.request.event.has_subevents: headers.append(pgettext('subevent', 'Date')) @@ -201,6 +225,7 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView): status, w.voucher.code if w.voucher else '', w.locale, + str(w.priority) ] if self.request.event.has_subevents: row.append(str(w.subevent)) diff --git a/src/tests/control/test_waitinglist.py b/src/tests/control/test_waitinglist.py index f376c0fef3..2995f46892 100644 --- a/src/tests/control/test_waitinglist.py +++ b/src/tests/control/test_waitinglist.py @@ -104,6 +104,29 @@ def test_assign_single(client, env): 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 def test_delete_single(client, env): client.login(email='dummy@dummy.dummy', password='dummy')