From 20da00d4fb0de853372cf288ce7af63064890ce1 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 20 Apr 2026 23:52:19 +0200 Subject: [PATCH] More tests --- src/pretix/control/forms/vouchers.py | 59 +++--- src/pretix/control/views/vouchers.py | 11 +- src/tests/control/test_vouchers.py | 285 ++++++++++++++++++++++++--- 3 files changed, 291 insertions(+), 64 deletions(-) diff --git a/src/pretix/control/forms/vouchers.py b/src/pretix/control/forms/vouchers.py index 3318e3e99f..0a4f95f5a8 100644 --- a/src/pretix/control/forms/vouchers.py +++ b/src/pretix/control/forms/vouchers.py @@ -33,13 +33,13 @@ # License for the specific language governing permissions and limitations under the License. import csv -from collections import namedtuple, Counter +from collections import Counter, namedtuple from io import StringIO from django import forms from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import EmailValidator -from django.db.models import Max, Sum, Count, Q, F +from django.db.models import Count, F, Max from django.db.models.functions import Upper from django.forms.utils import ErrorDict from django.urls import reverse @@ -53,7 +53,7 @@ from pretix.base.forms import ( ) from pretix.base.forms.widgets import format_placeholders_help_text from pretix.base.i18n import language -from pretix.base.models import Item, Voucher, Quota, SubEvent, ItemVariation +from pretix.base.models import Item, ItemVariation, Quota, SubEvent, Voucher from pretix.base.services.locking import lock_objects from pretix.base.services.quotas import QuotaAvailability from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget @@ -283,10 +283,10 @@ class VoucherBulkEditForm(VoucherForm): def clean(self): # We skip the parent class because it's not suited for bulk editing and implement custom validation here. - # This does not validate everything we validate in VoucherForm. For example, we skip validation that one does - # not create a voucher for an add-on product to save on complexity. This is a UX validation only anyways, since - # one could first create the voucher and then make the product an add-on product. However, we need to validate - # everything that we don't want violated in the database. + # This does not validate *everything* we validate in VoucherForm. For example, we skip validation that one does + # not create a voucher for an add-on product or that the seat matches the product to save on complexity. + # This is a UX validation only anyway, since one could first create the voucher and then make the product an + # add-on product. However, we need to validate everything that we don't want violated in the database. data = super(VoucherForm, self).clean() if self.prefix + "itemvar" in self.data.getlist('_bulk'): @@ -321,7 +321,7 @@ class VoucherBulkEditForm(VoucherForm): except ObjectDoesNotExist: raise ValidationError(_("Invalid product selected.")) - if self.prefix + "max_usages" in self.data.getlist('_bulk'): + if self.prefix + "max_usages" in self.data.getlist('_bulk') and "max_usages" in data: max_redeemed = self.queryset.aggregate(m=Max("redeemed"))["m"] if data["max_usages"] < max_redeemed: raise ValidationError(_( @@ -375,26 +375,26 @@ class VoucherBulkEditForm(VoucherForm): # Predict state after change after_change = dict(current) - if self.prefix + "itemvar" in self.data.getlist('_bulk'): + if self.prefix + "itemvar" in self.data.getlist('_bulk') and "itemvar" in data: after_change["item"] = data["item"] after_change["variation"] = data["variation"] after_change["quota"] = data["quota"] - if self.prefix + "subevent" in self.data.getlist('_bulk'): + if self.prefix + "subevent" in self.data.getlist('_bulk') and "subevent" in data: after_change["subevent"] = data["subevent"] - if self.prefix + "max_usages" in self.data.getlist('_bulk'): + if self.prefix + "max_usages" in self.data.getlist('_bulk') and "max_usages" in data: after_change["max_usages"] = data["max_usages"] - if self.prefix + "block_quota" in self.data.getlist('_bulk'): + if self.prefix + "block_quota" in self.data.getlist('_bulk') and "block_quota" in data: after_change["block_quota"] = data["block_quota"] - if self.prefix + "valid_until" in self.data.getlist('_bulk'): + if self.prefix + "valid_until" in self.data.getlist('_bulk') and "valid_until" in data: after_change["valid_until"] = data["valid_until"] - if self.prefix + "allow_ignore_quota" in self.data.getlist('_bulk'): + if self.prefix + "allow_ignore_quota" in self.data.getlist('_bulk') and "allow_ignore_quota" in data: after_change["allow_ignore_quota"] = data["allow_ignore_quota"] if after_change["quota"] and self.event.has_subevents and not after_change["subevent"]: - raise _("You cannot create a voucher that allows selection of a quota but has no date selected.") + raise ValidationError(_("You cannot create a voucher that allows selection of a quota but has no date selected.")) if after_change["quota"] and after_change["subevent"] and after_change["quota"].subevent_id != after_change["subevent"].pk: - raise _("The selected quota does not match the selected subevent.") + raise ValidationError(_("The selected quota does not match the selected subevent.")) if after_change["block_quota"] and self.event.has_subevents and not after_change["subevent"]: raise ValidationError( @@ -410,7 +410,7 @@ class VoucherBulkEditForm(VoucherForm): # todo: is this the most useful way to do this? continue - will_be_valid = current["valid_until"] is None or current["valid_until"] >= now() + will_be_valid = after_change["valid_until"] is None or after_change["valid_until"] >= now() new_quotas = set() if will_be_valid and after_change["block_quota"] and after_change["max_usages"] > current["redeemed"]: if after_change["quota"]: @@ -428,7 +428,7 @@ class VoucherBulkEditForm(VoucherForm): else: new_quotas |= set(after_change["item"].quotas.filter(subevent=after_change["subevent"])) - new_amount = max(current["max_usages"] - current["redeemed"], 0) + new_amount = max(after_change["max_usages"] - after_change["redeemed"], 0) * current["c"] if new_quotas != old_quotas or new_amount != old_amount: for q in old_quotas: quota_diff[q] -= old_amount @@ -441,7 +441,8 @@ class VoucherBulkEditForm(VoucherForm): qa.queue(*(q for q, v in quota_diff.items() if v > 0)) qa.compute() - if any(qa.results[q][0] != Quota.AVAILABILITY_OK or (qa.results[q][1] is not None and qa.results[q][1] < required) for q, required in quota_diff.items() if required > 0): + if any(qa.results[q][0] != Quota.AVAILABILITY_OK or (qa.results[q][1] is not None and qa.results[q][1] < required) + for q, required in quota_diff.items() if required > 0): raise ValidationError(_( 'There is no sufficient quota available to perform this change.' )) @@ -473,27 +474,27 @@ class VoucherBulkEditForm(VoucherForm): valid_until__lt=now(), ) if self.event.has_subevents: - conflicts = currently_not_blocked_seats.exclude( - seat_id__in=self.event.free_seats.values("pk") - ) - if conflicts: - raise ValidationError(_( - 'This change cannot be completed because not all assigned seats of the vouchers are ' - 'still available' - )) - else: subevents = self.event.subevents.filter(pk__in=currently_not_blocked_seats.values_list("subevent")) for se in subevents: conflicts = currently_not_blocked_seats.filter( subevent=se ).exclude( - seat_id__in=se.free_seats.values("pk") + seat_id__in=se.free_seats().values("pk") ) if conflicts: raise ValidationError(_( 'This change cannot be completed because not all assigned seats of the vouchers are ' 'still available' )) + else: + conflicts = currently_not_blocked_seats.exclude( + seat_id__in=self.event.free_seats().values("pk") + ) + if conflicts: + raise ValidationError(_( + 'This change cannot be completed because not all assigned seats of the vouchers are ' + 'still available' + )) return data diff --git a/src/pretix/control/views/vouchers.py b/src/pretix/control/views/vouchers.py index 2572fa0dda..ff73422f84 100644 --- a/src/pretix/control/views/vouchers.py +++ b/src/pretix/control/views/vouchers.py @@ -42,7 +42,7 @@ from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied, ValidationError from django.db import connection, transaction -from django.db.models import Exists, OuterRef, Sum, Subquery, Count +from django.db.models import Count, Exists, OuterRef, Sum from django.http import ( Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse, @@ -55,7 +55,7 @@ from django.utils.safestring import mark_safe from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django.views.generic import ( - CreateView, ListView, TemplateView, UpdateView, View, FormView, + CreateView, FormView, ListView, TemplateView, UpdateView, View, ) from django_scopes import scopes_disabled @@ -70,7 +70,9 @@ from pretix.base.services.vouchers import vouchers_send from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.views.tasks import AsyncFormView from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm -from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm, VoucherBulkEditForm +from pretix.control.forms.vouchers import ( + VoucherBulkEditForm, VoucherBulkForm, VoucherForm, +) from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.signals import voucher_form_class from pretix.control.views import PaginationMixin @@ -671,7 +673,6 @@ class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View): }) - class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, FormView): template_name = 'pretixcontrol/vouchers/bulk_edit.html' permission = 'event.vouchers:write' @@ -757,7 +758,7 @@ class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, For data['_raw_bulk_data'] = self.request.POST.dict() for obj in self.get_queryset(): log_entries.append( - obj.log_action('pretix.event.quota.changed', data=data, user=self.request.user, save=False) + obj.log_action('pretix.voucher.changed', data=data, user=self.request.user, save=False) ) LogEntry.bulk_create_and_postprocess(log_entries) diff --git a/src/tests/control/test_vouchers.py b/src/tests/control/test_vouchers.py index db1d810847..461fa6909f 100644 --- a/src/tests/control/test_vouchers.py +++ b/src/tests/control/test_vouchers.py @@ -44,8 +44,8 @@ from django_scopes import scopes_disabled from tests.base import SoupTestMixin, extract_form_fields from pretix.base.models import ( - Event, Item, ItemVariation, Order, OrderPosition, Organizer, Quota, Team, - User, Voucher, + Event, Item, ItemVariation, Order, OrderPosition, Organizer, Quota, + SeatingPlan, Team, User, Voucher, ) @@ -135,49 +135,49 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase): def test_filter_status_valid(self): with scopes_disabled(): v = self.event.vouchers.create(item=self.ticket) - doc = self.client.get('/control/event/%s/%s/vouchers/?status=v' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=v' % (self.orga.slug, self.event.slug)) assert v.code in doc.content.decode() v.redeemed = 1 v.save() - doc = self.client.get('/control/event/%s/%s/vouchers/?status=v' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=v' % (self.orga.slug, self.event.slug)) assert v.code not in doc.content.decode() def test_filter_status_redeemed(self): with scopes_disabled(): v = self.event.vouchers.create(item=self.ticket, redeemed=1) - doc = self.client.get('/control/event/%s/%s/vouchers/?status=r' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=r' % (self.orga.slug, self.event.slug)) assert v.code in doc.content.decode() v.redeemed = 0 v.save() - doc = self.client.get('/control/event/%s/%s/vouchers/?status=r' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=r' % (self.orga.slug, self.event.slug)) assert v.code not in doc.content.decode() def test_filter_status_expired(self): with scopes_disabled(): v = self.event.vouchers.create(item=self.ticket, valid_until=now() + datetime.timedelta(days=1)) - doc = self.client.get('/control/event/%s/%s/vouchers/?status=e' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=e' % (self.orga.slug, self.event.slug)) assert v.code not in doc.content.decode() v.valid_until = now() - datetime.timedelta(days=1) v.save() - doc = self.client.get('/control/event/%s/%s/vouchers/?status=e' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-status=e' % (self.orga.slug, self.event.slug)) assert v.code in doc.content.decode() def test_filter_tag(self): with scopes_disabled(): self.event.vouchers.create(item=self.ticket, code='ABCDEFG', comment='Foo', tag='bar') - doc = self.client.get('/control/event/%s/%s/vouchers/?tag=bar' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-tag=bar' % (self.orga.slug, self.event.slug)) assert 'ABCDEFG' in doc.content.decode() - doc = self.client.get('/control/event/%s/%s/vouchers/?tag=baz' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-tag=baz' % (self.orga.slug, self.event.slug)) assert 'ABCDEFG' not in doc.content.decode() def test_search_code(self): with scopes_disabled(): self.event.vouchers.create(item=self.ticket, code='ABCDEFG', comment='Foo') - doc = self.client.get('/control/event/%s/%s/vouchers/?search=ABCDEFG' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=ABCDEFG' % (self.orga.slug, self.event.slug)) assert 'ABCDEFG' in doc.content.decode() - doc = self.client.get('/control/event/%s/%s/vouchers/?search=Foo' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=Foo' % (self.orga.slug, self.event.slug)) assert 'ABCDEFG' in doc.content.decode() - doc = self.client.get('/control/event/%s/%s/vouchers/?search=12345' % (self.orga.slug, self.event.slug)) + doc = self.client.get('/control/event/%s/%s/vouchers/?filter-search=12345' % (self.orga.slug, self.event.slug)) assert 'ABCDEFG' not in doc.content.decode() def test_bulk_rng(self): @@ -851,11 +851,12 @@ class VoucherBulkEditFormTest(SoupTestMixin, TransactionTestCase): fields = extract_form_fields(doc) fields.update(data) doc = self.post_doc(self.url, fields, follow=True) + error_texts = [el.text for el in doc.select(".alert-danger, .has-error")] if expect_error: assert doc.select(".alert-danger") - assert any(expect_error in el.text for el in doc.select(".alert-danger")) + assert any(expect_error in t for t in error_texts), error_texts else: - assert doc.select(".alert-success") + assert doc.select(".alert-success"), error_texts def test_change_itemvar_to_product(self): with scopes_disabled(): @@ -902,6 +903,21 @@ class VoucherBulkEditFormTest(SoupTestMixin, TransactionTestCase): assert not v.variation assert v.quota == self.quota_tickets + def test_change_itemvar_to_all(self): + with scopes_disabled(): + self.event.vouchers.create(quota=self.quota_tickets) + self.event.vouchers.create(item=self.ticket) + + self._update_all({ + '_bulk': ['bulkedititemvar'], + 'bulkedit-itemvar': '', + }) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert not v.item + assert not v.variation + assert not v.quota + def test_change_max_usages(self): with scopes_disabled(): self.event.vouchers.create(quota=self.quota_tickets, max_usages=15, redeemed=4) @@ -919,10 +935,11 @@ class VoucherBulkEditFormTest(SoupTestMixin, TransactionTestCase): for v in self.event.vouchers.all(): assert v.max_usages == 4 - def _requires_one_more_quota(self, data: dict, expect_error: str=None): + def _requires_one_more_quota(self, data: dict, quota=None, expect_error: str=None): self._update_all(data, expect_error="no sufficient quota") - self.quota_tickets.size += 1 - self.quota_tickets.save() + quota = quota or self.quota_tickets + quota.size += 1 + quota.save() self._update_all(data) def test_quota_check_change_item(self): @@ -937,10 +954,36 @@ class VoucherBulkEditFormTest(SoupTestMixin, TransactionTestCase): for v in self.event.vouchers.all(): assert v.item == self.ticket + def test_quota_check_change_variation(self): + with scopes_disabled(): + self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2, redeemed=1) + self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=3, redeemed=1) + self._requires_one_more_quota({ + '_bulk': ['bulkedititemvar'], + 'bulkedit-itemvar': f'{self.shirt.pk}-{self.shirt_red.pk}', + }, quota=self.quota_shirts) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert v.item == self.shirt + assert v.variation == self.shirt_red + + def test_quota_check_change_item_with_variations(self): + with scopes_disabled(): + self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2, redeemed=1) + self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=3, redeemed=1) + self._requires_one_more_quota({ + '_bulk': ['bulkedititemvar'], + 'bulkedit-itemvar': f'{self.shirt.pk}', + }, quota=self.quota_shirts) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert v.item == self.shirt + assert not v.variation + def test_quota_check_change_expired_to_valid(self): with scopes_disabled(): - self.event.vouchers.create(item=self.shirt, block_quota=True, max_usages=2) - self.event.vouchers.create(item=self.shirt, block_quota=True, max_usages=1, valid_until=now() - datetime.timedelta(days=1)) + self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2) + self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=1, valid_until=now() - datetime.timedelta(days=1)) self._requires_one_more_quota({ '_bulk': ['bulkeditvalid_until'], 'bulkedit-valid_until_0': '', @@ -952,20 +995,202 @@ class VoucherBulkEditFormTest(SoupTestMixin, TransactionTestCase): def test_quota_check_change_max_usages(self): with scopes_disabled(): - self.event.vouchers.create(item=self.shirt, block_quota=True, max_usages=2) - self.event.vouchers.create(item=self.shirt, block_quota=True, max_usages=1, redeemed=1) + self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=2) + self.event.vouchers.create(item=self.ticket, block_quota=True, max_usages=1, redeemed=1) self._requires_one_more_quota({ '_bulk': ['bulkeditmax_usages'], - 'bulkedit-max_usages': '', + 'bulkedit-max_usages': '2', }) with scopes_disabled(): for v in self.event.vouchers.all(): assert v.max_usages == 2 - # test quota use existing credit - # test quota changed subevent - # test quota changed subevent to mismatch quota - # test quota changed subevent to none - # test quota changed block quota, ignore - # test change seat properties - # test seats still available after validity change \ No newline at end of file + def test_quota_check_no_change(self): + with scopes_disabled(): + # Technically overbooked, but we don't have a diff in quota + self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True) + self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True) + self.event.vouchers.create(item=self.shirt, variation=self.shirt_red, block_quota=True) + self._update_all({ + '_bulk': ['bulkedititemvar'], + 'bulkedit-itemvar': f'{self.shirt.pk}-{self.shirt_blue.pk}', + }) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert v.variation == self.shirt_blue + + def test_quota_check_change_subevent(self): + with scopes_disabled(): + self.event.has_subevents = True + self.event.save() + se1 = self.event.subevents.create(name="Foo", date_from=now()) + se2 = self.event.subevents.create(name="Bar", date_from=now()) + self.quota_tickets.subevent = se1 + self.quota_tickets.save() + Quota.objects.create(event=self.event, subevent=se2, name='Tickets', size=3) + self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2) + self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2) + self.event.vouchers.create(item=self.ticket, block_quota=True, subevent=se2) + self._requires_one_more_quota({ + '_bulk': ['bulkeditsubevent'], + 'bulkedit-subevent': f'{se1.pk}', + }) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert v.subevent == se1 + + def test_change_subevent_quota_invalid(self): + with scopes_disabled(): + self.event.has_subevents = True + self.event.save() + se1 = self.event.subevents.create(name="Foo", date_from=now()) + se2 = self.event.subevents.create(name="Bar", date_from=now()) + self.quota_tickets.subevent = se1 + self.quota_tickets.save() + v1 = self.event.vouchers.create(quota=self.quota_tickets, block_quota=True, subevent=se1) + self._update_all({ + '_bulk': ['bulkeditsubevent'], + 'bulkedit-subevent': f'{se2.pk}', + }, expect_error="selected quota does not match the selected subevent") + self._update_all({ + '_bulk': ['bulkeditsubevent'], + 'bulkedit-subevent': '', + }, expect_error="has no date selected") + v1.quota = None + v1.item = self.ticket + v1.save() + self._update_all({ + '_bulk': ['bulkeditsubevent'], + 'bulkedit-subevent': '', + }, expect_error="If you want this voucher to block quota, you need to select a specific date") + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert v.subevent == se1 + + def test_change_missing_itemvar_with_block_quota(self): + with scopes_disabled(): + self.event.vouchers.create(quota=self.quota_tickets, block_quota=True) + self.event.vouchers.create(quota=self.quota_tickets, block_quota=True) + self._update_all({ + '_bulk': ['bulkedititemvar'], + 'bulkedit-itemvar': '', + }, expect_error="You need to select a specific product or quota if this voucher should reserve") + self._update_all({ + '_bulk': ['bulkedititemvar', 'bulkeditblock_quota'], + 'bulkedit-itemvar': '', + 'bulkedit-block_quota': '', + }) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert not v.subevent + assert not v.block_quota + + def test_change_subevent_and_quota(self): + with scopes_disabled(): + self.event.has_subevents = True + self.event.save() + se1 = self.event.subevents.create(name="Foo", date_from=now()) + se2 = self.event.subevents.create(name="Bar", date_from=now()) + self.quota_tickets.subevent = se1 + self.quota_tickets.save() + q2 = Quota.objects.create(event=self.event, subevent=se2, name='Tickets', size=3) + self.event.vouchers.create(quota=self.quota_tickets, block_quota=True, subevent=se1) + self._update_all({ + '_bulk': ['bulkedititemvar', 'bulkeditsubevent'], + 'bulkedit-subevent': f'{se2.pk}', + 'bulkedit-itemvar': f'q-{q2.pk}', + }) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert v.subevent == se2 + assert v.quota == q2 + + def test_quota_check_change_block_quota(self): + with scopes_disabled(): + self.event.vouchers.create(item=self.ticket, max_usages=3) + self._requires_one_more_quota({ + '_bulk': ['bulkeditblock_quota'], + 'bulkedit-block_quota': 'on', + }) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert v.block_quota + + def test_ignore_quota(self): + with scopes_disabled(): + self.event.vouchers.create(item=self.ticket, max_usages=3) + self._update_all({ + '_bulk': ['bulkeditblock_quota', 'bulkeditallow_ignore_quota'], + 'bulkedit-block_quota': 'on', + 'bulkedit-allow_ignore_quota': 'on', + }) + with scopes_disabled(): + for v in self.event.vouchers.all(): + assert v.block_quota + assert v.allow_ignore_quota + + @scopes_disabled() + def _create_seat(self, **kwargs): + plan = SeatingPlan.objects.create( + name="Plan", organizer=self.orga, layout="{}" + ) + self.event.seating_plan = plan + self.event.save() + return self.event.seats.create(seat_number="A1", product=self.ticket, seat_guid="A1", **kwargs) + + def test_seated_unsupported(self): + with scopes_disabled(): + self.event.vouchers.create(item=self.ticket, max_usages=1, seat=self._create_seat()) + self._update_all({ + '_bulk': ['bulkeditmax_usages'], + 'bulkedit-max_usages': '2', + }, expect_error="Changing the maximum number of usages in bulk is not supported") + self._update_all({ + '_bulk': ['bulkeditsubevent'], + 'bulkedit-subevent': '', + }, expect_error="Changing the date in bulk is not supported") + self._update_all({ + '_bulk': ['bulkedititemvar'], + 'bulkedit-itemvar': f'q-{self.quota_tickets.pk}', + }, expect_error="Changing the product to a quota is not supported") + + def test_seat_changed_to_valid_needs_to_be_available(self): + with scopes_disabled(): + seat = self._create_seat(blocked=True) + self.event.vouchers.create(item=self.ticket, max_usages=1, valid_until=now() - datetime.timedelta(days=1), seat=seat) + + self._update_all({ + '_bulk': ['bulkeditvalid_until'], + 'bulkedit-valid_until_0': '', + 'bulkedit-valid_until_1': '', + }, expect_error="not all assigned seats of the vouchers are still available") + + seat.blocked = False + seat.save() + self._update_all({ + '_bulk': ['bulkeditvalid_until'], + 'bulkedit-valid_until_0': '', + 'bulkedit-valid_until_1': '', + }) + + def test_seat_changed_to_valid_needs_to_be_available_subevents(self): + with scopes_disabled(): + self.event.has_subevents = True + self.event.save() + se1 = self.event.subevents.create(name="Foo", date_from=now()) + seat = self._create_seat(subevent=se1, blocked=True) + self.event.vouchers.create(item=self.ticket, max_usages=1, valid_until=now() - datetime.timedelta(days=1), seat=seat, subevent=se1) + + self._update_all({ + '_bulk': ['bulkeditvalid_until'], + 'bulkedit-valid_until_0': '', + 'bulkedit-valid_until_1': '', + }, expect_error="not all assigned seats of the vouchers are still available") + + seat.blocked = False + seat.save() + self._update_all({ + '_bulk': ['bulkeditvalid_until'], + 'bulkedit-valid_until_0': '', + 'bulkedit-valid_until_1': '', + })